2026-06-18 19:26:48 +00:00
import { useEffect , useRef , useState } from 'react'
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
import { useSearchParams } from 'react-router-dom'
Add auth Phase 2: password change, sessions, and login audit log (#27)
Builds out the Settings → Security tab (previously a "coming soon"
placeholder) and the backend behind it. Still single-user; multi-user
and SSO remain Phases 3-4.
Backend:
- New `sessions` table (id, user_id, user_agent, ip, created_at,
last_seen_at) and `login_events` table (user_id, username, ip,
user_agent, success, created_at).
- Login and setup now mint a session row and embed its id as a `sid`
claim in the JWT. The `authenticate` hook validates that the session
still exists (and bumps last_seen_at), so revoking a session genuinely
invalidates its token instead of relying on the JWT signature alone.
Tokens minted before sessions existed have no `sid` and stay valid
until expiry, for backward compatibility.
- Every login attempt (success and failure) is recorded in login_events
for the audit trail.
- New endpoints: PUT /api/auth/password (verifies current via bcrypt,
hashes new at cost 12, revokes all *other* sessions on success),
GET /api/auth/sessions, DELETE /api/auth/sessions/:id (can't revoke
the current one), POST /api/auth/logout (revokes current session),
GET /api/auth/login-events?limit.
- AuthContext.logout() now calls POST /api/auth/logout best-effort so
signing out revokes the server session, not just the local token.
Frontend:
- SecuritySection: change-password form (current/new/confirm with
show/hide and client-side validation), active-sessions list (device
description from user-agent, IP, last-seen relative time, per-session
"Sign out" for non-current sessions), and a recent login-activity feed
(success/failure dot, user, IP, relative time).
- api.ts: changePassword/listSessions/revokeSession/logout/
listLoginEvents + AuthSession/LoginEvent types.
Verified end-to-end against a throwaway backend instance: session
creation, second-device session, failed-login logging, cross-session
revocation invalidating the revoked token, password change keeping the
current session alive while revoking others, and logout invalidating the
current session. Frontend + backend both type-check clean.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
2026-06-20 11:50:56 -04:00
import { api , ApiError , type Integration , type AuthSession , type LoginEvent } from '../lib/api'
2026-06-18 20:08:30 +00:00
import { useAuth } from '../lib/AuthContext'
2026-06-18 18:44:26 +00:00
import {
User ,
Palette ,
Plug ,
Bell ,
Database ,
Info ,
Eye ,
EyeOff ,
Check ,
Download ,
Upload ,
Trash2 ,
2026-06-18 18:50:43 +00:00
Camera ,
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
ChevronDown ,
ChevronRight ,
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
Shield ,
Add auth Phase 2: password change, sessions, and login audit log (#27)
Builds out the Settings → Security tab (previously a "coming soon"
placeholder) and the backend behind it. Still single-user; multi-user
and SSO remain Phases 3-4.
Backend:
- New `sessions` table (id, user_id, user_agent, ip, created_at,
last_seen_at) and `login_events` table (user_id, username, ip,
user_agent, success, created_at).
- Login and setup now mint a session row and embed its id as a `sid`
claim in the JWT. The `authenticate` hook validates that the session
still exists (and bumps last_seen_at), so revoking a session genuinely
invalidates its token instead of relying on the JWT signature alone.
Tokens minted before sessions existed have no `sid` and stay valid
until expiry, for backward compatibility.
- Every login attempt (success and failure) is recorded in login_events
for the audit trail.
- New endpoints: PUT /api/auth/password (verifies current via bcrypt,
hashes new at cost 12, revokes all *other* sessions on success),
GET /api/auth/sessions, DELETE /api/auth/sessions/:id (can't revoke
the current one), POST /api/auth/logout (revokes current session),
GET /api/auth/login-events?limit.
- AuthContext.logout() now calls POST /api/auth/logout best-effort so
signing out revokes the server session, not just the local token.
Frontend:
- SecuritySection: change-password form (current/new/confirm with
show/hide and client-side validation), active-sessions list (device
description from user-agent, IP, last-seen relative time, per-session
"Sign out" for non-current sessions), and a recent login-activity feed
(success/failure dot, user, IP, relative time).
- api.ts: changePassword/listSessions/revokeSession/logout/
listLoginEvents + AuthSession/LoginEvent types.
Verified end-to-end against a throwaway backend instance: session
creation, second-device session, failed-login logging, cross-session
revocation invalidating the revoked token, password change keeping the
current session alive while revoking others, and logout invalidating the
current session. Frontend + backend both type-check clean.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
2026-06-20 11:50:56 -04:00
Monitor ,
LogOut ,
2026-06-18 18:44:26 +00:00
} from 'lucide-react'
const navSections = [
{ id : 'profile' , label : 'Profile' , icon : User } ,
{ id : 'appearance' , label : 'Appearance' , icon : Palette } ,
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
{ id : 'security' , label : 'Security' , icon : Shield } ,
2026-06-18 18:44:26 +00:00
{ id : 'integrations' , label : 'Integrations' , icon : Plug } ,
{ id : 'notifications' , label : 'Notifications' , icon : Bell } ,
{ id : 'data' , label : 'Data & Backup' , icon : Database } ,
{ id : 'about' , label : 'About' , icon : Info } ,
]
const accentColors = [
{ name : 'Gold' , color : '#C8A434' } ,
{ name : 'Teal' , color : '#2DD4BF' } ,
{ name : 'Purple' , color : '#A855F7' } ,
{ name : 'Blue' , color : '#3B82F6' } ,
{ name : 'Green' , color : '#2ECC71' } ,
{ name : 'Red' , color : '#E74C3C' } ,
]
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
type FieldDef = { key : string ; label : string ; secret? : boolean ; hint? : string ; placeholder? : string ; file? : boolean }
2026-06-20 10:18:04 +00:00
const integrationTypeDefs : { type : string ; name : string ; multiInstance? : boolean ; fields : FieldDef [ ] } [ ] = [
{ type : 'proxmox' , name : 'Proxmox' , multiInstance : true , fields : [
{ key : 'baseUrl' , label : 'Host URL' , hint : 'e.g. https://192.168.1.10:8006' , placeholder : 'https://192.168.1.10:8006' } ,
{
key : 'apiKey' ,
label : 'API Token' ,
secret : true ,
hint : 'Must be the FULL token string from Datacenter → Permissions → API Tokens, in the form USER@REALM!TOKENID=SECRET — not just the secret.' ,
placeholder : 'root@pam!archnest=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' ,
} ,
] } ,
{ type : 'docker' , name : 'Docker' , multiInstance : true , fields : [
{ key : 'baseUrl' , label : 'Socket / Remote URL' , hint : 'Unix socket path or remote daemon URL, e.g. unix:///var/run/docker.sock or tcp://host:2375' , placeholder : 'unix:///var/run/docker.sock' } ,
] } ,
{ type : 'netbird' , name : 'NetBird' , fields : [
{ key : 'apiKey' , label : 'API Key' , secret : true , hint : 'Personal access token from NetBird dashboard → Settings → Access Tokens.' } ,
] } ,
{ type : 'cloudflare' , name : 'Cloudflare' , fields : [
{ key : 'apiKey' , label : 'API Token' , secret : true , hint : 'A scoped API token (not your Global API Key) from My Profile → API Tokens.' } ,
{ key : 'zoneId' , label : 'Zone ID' , hint : 'Found on the domain overview page in the Cloudflare dashboard.' } ,
] } ,
{ type : 'aws' , name : 'AWS' , multiInstance : true , fields : [
{ key : 'accessKey' , label : 'Access Key ID' , hint : 'IAM user access key, e.g. AKIAIOSFODNN7EXAMPLE' } ,
{ key : 'secretKey' , label : 'Secret Access Key' , secret : true , hint : 'IAM user secret key — paired with the Access Key ID above.' } ,
{ key : 'region' , label : 'Region' , hint : 'e.g. us-east-1' , placeholder : 'us-east-1' } ,
] } ,
{ type : 'uptime_kuma' , name : 'Uptime Kuma' , fields : [
{ key : 'baseUrl' , label : 'URL' , placeholder : 'https://uptime.example.com' } ,
{ key : 'apiKey' , label : 'API Key' , secret : true } ,
] } ,
2026-06-18 19:26:48 +00:00
{ type : 'weather' , name : 'Weather API' , fields : [ { key : 'location' , label : 'Location' } , { key : 'units' , label : 'Units' } ] } ,
2026-06-20 10:18:04 +00:00
{ type : 'remote_desktop' , name : 'Remote Desktop' , multiInstance : true , fields : [
2026-06-19 15:25:10 +00:00
{ key : 'protocol' , label : 'Protocol (rdp / vnc / telnet)' } ,
{ key : 'hostname' , label : 'Hostname' } ,
{ key : 'port' , label : 'Port' } ,
{ key : 'username' , label : 'Username' } ,
{ key : 'domain' , label : 'Domain (RDP only)' } ,
{ key : 'password' , label : 'Password' , secret : true } ,
] } ,
2026-06-19 11:04:46 +00:00
]
const sshFields : FieldDef [ ] = [
{ key : 'host' , label : 'Host / IP' } ,
{ key : 'port' , label : 'Port (default 22)' } ,
{ key : 'username' , label : 'Username' } ,
{ key : 'password' , label : 'Password' , secret : true } ,
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
{ key : 'privateKey' , label : 'Private Key (PEM)' , secret : true , file : true } ,
2026-06-19 11:04:46 +00:00
{ key : 'passphrase' , label : 'Key Passphrase' , secret : true } ,
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
{ key : 'certificate' , label : 'OPKSSH Certificate (id_key-cert.pub)' , secret : true , file : true } ,
2026-06-18 18:44:26 +00:00
]
const cardBase : React.CSSProperties = {
backgroundColor : 'rgba(10, 10, 12, 0.92)' ,
border : '1px solid rgba(200, 164, 52, 0.08)' ,
borderRadius : '12px' ,
padding : '22px' ,
position : 'relative' ,
}
const sectionTitle : React.CSSProperties = {
fontSize : '11px' ,
textTransform : 'uppercase' ,
letterSpacing : '1.5px' ,
color : '#7A7D85' ,
fontWeight : 500 ,
marginBottom : '16px' ,
}
const labelStyle : React.CSSProperties = {
fontSize : '11px' ,
color : '#7A7D85' ,
marginBottom : '6px' ,
display : 'block' ,
}
const inputStyle : React.CSSProperties = {
width : '100%' ,
height : '34px' ,
borderRadius : '8px' ,
border : '1px solid rgba(200,164,52,0.12)' ,
backgroundColor : 'rgba(255,255,255,0.03)' ,
color : '#E8E6E0' ,
fontSize : '12px' ,
padding : '0 12px' ,
outline : 'none' ,
}
function Toggle ( { on , onClick } : { on : boolean ; onClick : ( ) = > void } ) {
return (
< button
onClick = { onClick }
className = "cursor-pointer border-none"
style = { {
width : '38px' ,
height : '20px' ,
borderRadius : '10px' ,
backgroundColor : on ? '#C8A434' : 'rgba(255,255,255,0.08)' ,
position : 'relative' ,
transition : 'background-color 0.2s ease' ,
flexShrink : 0 ,
} }
>
< span
style = { {
position : 'absolute' ,
top : '2px' ,
left : on ? '20px' : '2px' ,
width : '16px' ,
height : '16px' ,
borderRadius : '50%' ,
backgroundColor : '#0A0B0D' ,
transition : 'left 0.2s ease' ,
} }
/ >
< / button >
)
}
2026-06-18 20:08:30 +00:00
function GoldButton ( { children , danger , onClick , disabled } : { children : React.ReactNode ; danger? : boolean ; onClick ? : ( ) = > void ; disabled? : boolean } ) {
2026-06-18 18:44:26 +00:00
return (
< button
2026-06-18 20:08:30 +00:00
onClick = { onClick }
disabled = { disabled }
2026-06-18 18:44:26 +00:00
className = "flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
style = { {
fontSize : '12px' ,
fontWeight : 600 ,
color : danger ? '#E74C3C' : '#0A0B0D' ,
backgroundColor : danger ? 'transparent' : '#C8A434' ,
border : danger ? '1px solid rgba(231,76,60,0.4)' : 'none' ,
borderRadius : '8px' ,
padding : '9px 16px' ,
boxShadow : danger ? 'none' : '0 0 14px rgba(200,164,52,0.2)' ,
2026-06-18 20:08:30 +00:00
opacity : disabled ? 0.6 : 1 ,
2026-06-18 18:44:26 +00:00
} }
>
{ children }
< / button >
)
}
function ProfileSection() {
2026-06-18 20:08:30 +00:00
const { user , setUser } = useAuth ( )
2026-06-18 18:50:43 +00:00
const fileInputRef = useRef < HTMLInputElement > ( null )
2026-06-18 20:08:30 +00:00
const [ displayName , setDisplayName ] = useState ( user ? . display_name ? ? '' )
const [ email , setEmail ] = useState ( user ? . email ? ? '' )
const [ avatar , setAvatar ] = useState < string | null > ( user ? . avatar_data_url ? ? null )
const [ saving , setSaving ] = useState ( false )
const [ savedMsg , setSavedMsg ] = useState ( '' )
useEffect ( ( ) = > {
setDisplayName ( user ? . display_name ? ? '' )
setEmail ( user ? . email ? ? '' )
setAvatar ( user ? . avatar_data_url ? ? null )
} , [ user ] )
const initials = ( displayName || user ? . username || '?' ) . slice ( 0 , 2 ) . toUpperCase ( )
2026-06-18 18:50:43 +00:00
function handleAvatarChange ( e : React.ChangeEvent < HTMLInputElement > ) {
const file = e . target . files ? . [ 0 ]
if ( ! file ) return
const reader = new FileReader ( )
reader . onload = ( ) = > setAvatar ( reader . result as string )
reader . readAsDataURL ( file )
}
2026-06-18 20:08:30 +00:00
async function handleSave() {
setSaving ( true )
setSavedMsg ( '' )
try {
const { user : updated } = await api . updateMe ( { displayName , email , avatarDataUrl : avatar } )
setUser ( updated )
setSavedMsg ( 'Saved' )
} catch ( err ) {
setSavedMsg ( err instanceof ApiError ? err . message : 'Failed to save' )
} finally {
setSaving ( false )
}
}
2026-06-18 18:44:26 +00:00
return (
< div style = { cardBase } >
< h3 style = { sectionTitle } > Profile < / h3 >
< div className = "flex items-center gap-4" style = { { marginBottom : '24px' } } >
< div
2026-06-18 18:50:43 +00:00
onClick = { ( ) = > fileInputRef . current ? . click ( ) }
className = "relative rounded-full border-2 flex items-center justify-center font-bold cursor-pointer group"
style = { {
width : '64px' ,
height : '64px' ,
borderColor : '#C8A434' ,
color : '#C8A434' ,
fontSize : '20px' ,
backgroundColor : 'rgba(200,164,52,0.08)' ,
backgroundImage : avatar ? ` url( ${ avatar } ) ` : undefined ,
backgroundSize : 'cover' ,
backgroundPosition : 'center' ,
overflow : 'hidden' ,
} }
title = "Upload photo"
2026-06-18 18:44:26 +00:00
>
2026-06-18 20:08:30 +00:00
{ ! avatar && initials }
2026-06-18 18:50:43 +00:00
< div
className = "absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
style = { { backgroundColor : 'rgba(0,0,0,0.55)' } }
>
< Camera size = { 18 } color = "#E8E6E0" / >
< / div >
2026-06-18 18:44:26 +00:00
< / div >
2026-06-18 18:50:43 +00:00
< input ref = { fileInputRef } type = "file" accept = "image/*" onChange = { handleAvatarChange } className = "hidden" / >
2026-06-18 18:44:26 +00:00
< div >
2026-06-18 20:08:30 +00:00
< span style = { { fontSize : '15px' , color : '#E8E6E0' , fontWeight : 600 } } > { displayName || user ? . username } < / span >
< br / >
< span style = { { fontSize : '12px' , color : '#7A7D85' } } > { email || 'No email set' } < / span >
2026-06-18 18:44:26 +00:00
< / div >
< / div >
< div className = "grid grid-cols-2 gap-4" style = { { marginBottom : '20px' } } >
< div >
< label style = { labelStyle } > Display Name < / label >
2026-06-18 20:08:30 +00:00
< input style = { inputStyle } value = { displayName } onChange = { ( e ) = > setDisplayName ( e . target . value ) } / >
2026-06-18 18:44:26 +00:00
< / div >
< div >
< label style = { labelStyle } > Email < / label >
2026-06-18 20:08:30 +00:00
< input style = { inputStyle } value = { email } onChange = { ( e ) = > setEmail ( e . target . value ) } / >
2026-06-18 18:44:26 +00:00
< / div >
< / div >
2026-06-18 20:08:30 +00:00
< div className = "flex items-center gap-3" >
< GoldButton onClick = { handleSave } disabled = { saving } >
< Check size = { 14 } / >
{ saving ? 'Saving…' : 'Save Changes' }
< / GoldButton >
{ savedMsg && < span style = { { fontSize : '12px' , color : '#7A7D85' } } > { savedMsg } < / span > }
< / div >
2026-06-18 18:44:26 +00:00
< / div >
)
}
function AppearanceSection() {
const [ theme , setTheme ] = useState < 'dark' | 'light' > ( 'dark' )
const [ accent , setAccent ] = useState ( 'Gold' )
const [ fontSize , setFontSize ] = useState ( 13 )
const [ radius , setRadius ] = useState ( 12 )
const [ sidebarExpanded , setSidebarExpanded ] = useState ( true )
const [ animations , setAnimations ] = useState ( true )
return (
< div style = { cardBase } >
< h3 style = { sectionTitle } > Appearance < / h3 >
< div className = "flex items-center justify-between" style = { { marginBottom : '20px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Theme < / span >
< div className = "flex items-center gap-1" style = { { backgroundColor : 'rgba(255,255,255,0.03)' , borderRadius : '8px' , padding : '3px' } } >
{ ( [ 'dark' , 'light' ] as const ) . map ( ( t ) = > (
< button
key = { t }
onClick = { ( ) = > setTheme ( t ) }
className = "cursor-pointer border-none capitalize"
style = { {
fontSize : '11px' ,
padding : '6px 14px' ,
borderRadius : '6px' ,
color : theme === t ? '#0A0B0D' : '#7A7D85' ,
backgroundColor : theme === t ? '#C8A434' : 'transparent' ,
fontWeight : 600 ,
} }
>
{ t }
< / button >
) ) }
< / div >
< / div >
< div style = { { marginBottom : '20px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' , marginBottom : '10px' , display : 'block' } } > Accent Color < / span >
< div className = "flex items-center gap-3" >
{ accentColors . map ( ( a ) = > (
< button
key = { a . name }
onClick = { ( ) = > setAccent ( a . name ) }
title = { a . name }
className = "cursor-pointer border-none rounded-full flex items-center justify-center"
style = { {
width : '28px' ,
height : '28px' ,
backgroundColor : a.color ,
outline : accent === a . name ? ` 2px solid ${ a . color } ` : 'none' ,
outlineOffset : '3px' ,
} }
>
{ accent === a . name && < Check size = { 14 } color = "#0A0B0D" / > }
< / button >
) ) }
< / div >
< / div >
< div style = { { marginBottom : '20px' } } >
< div className = "flex items-center justify-between" style = { { marginBottom : '8px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Font Size < / span >
< span style = { { fontSize : '11px' , color : '#C8A434' } } > { fontSize } px < / span >
< / div >
< input
type = "range"
min = { 12 }
max = { 16 }
value = { fontSize }
onChange = { ( e ) = > setFontSize ( Number ( e . target . value ) ) }
className = "w-full"
style = { { accentColor : '#C8A434' } }
/ >
< / div >
< div style = { { marginBottom : '20px' } } >
< div className = "flex items-center justify-between" style = { { marginBottom : '8px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Card Border Radius < / span >
< span style = { { fontSize : '11px' , color : '#C8A434' } } > { radius } px < / span >
< / div >
< input
type = "range"
min = { 4 }
max = { 16 }
value = { radius }
onChange = { ( e ) = > setRadius ( Number ( e . target . value ) ) }
className = "w-full"
style = { { accentColor : '#C8A434' } }
/ >
< / div >
< div className = "flex items-center justify-between" style = { { marginBottom : '16px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Sidebar Expanded by Default < / span >
< Toggle on = { sidebarExpanded } onClick = { ( ) = > setSidebarExpanded ( ( v ) = > ! v ) } / >
< / div >
< div className = "flex items-center justify-between" >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Animations < / span >
< Toggle on = { animations } onClick = { ( ) = > setAnimations ( ( v ) = > ! v ) } / >
< / div >
< / div >
)
}
2026-06-19 11:04:46 +00:00
function SshHostsSection() {
const [ hosts , setHosts ] = useState < Integration [ ] | null > ( null )
const [ revealed , setRevealed ] = useState < Set < string > > ( new Set ( ) )
const [ drafts , setDrafts ] = useState < Record < number , Record < string , string > > > ( { } )
const [ statusMsg , setStatusMsg ] = useState < Record < number , string > > ( { } )
const [ busy , setBusy ] = useState < Set < number > > ( new Set ( ) )
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
const [ collapsed , setCollapsed ] = useState < Set < number > > ( new Set ( ) )
2026-06-19 11:04:46 +00:00
const [ newDrafts , setNewDrafts ] = useState < { key : number ; values : Record < string , string > } [ ] > ( [ ] )
const nextNewKey = useRef ( - 1 )
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
const fileInputRefs = useRef < Record < string , HTMLInputElement | null > > ( { } )
Default-collapse already-configured SSH hosts on page load (#19)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Default-collapse already-configured SSH hosts on page load
Previously every SSH host card reset to expanded on each page visit,
showing blank secret fields that looked like saved keys had been
deleted. Hosts with saved secrets now start collapsed on first load.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:00:04 -04:00
const collapseInitialized = useRef ( false )
2026-06-19 11:04:46 +00:00
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
function toggleCollapsed ( id : number ) {
setCollapsed ( ( prev ) = > {
const next = new Set ( prev )
if ( next . has ( id ) ) next . delete ( id )
else next . add ( id )
return next
} )
}
2026-06-19 11:04:46 +00:00
useEffect ( ( ) = > {
refresh ( )
} , [ ] )
function refresh() {
Default-collapse already-configured SSH hosts on page load (#19)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Default-collapse already-configured SSH hosts on page load
Previously every SSH host card reset to expanded on each page visit,
showing blank secret fields that looked like saved keys had been
deleted. Hosts with saved secrets now start collapsed on first load.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:00:04 -04:00
api . listIntegrations ( ) . then ( ( { integrations } ) = > {
const sshHosts = integrations . filter ( ( i ) = > i . type === 'ssh' )
setHosts ( sshHosts )
if ( ! collapseInitialized . current ) {
collapseInitialized . current = true
setCollapsed ( new Set ( sshHosts . filter ( ( h ) = > h . secretKeys . length > 0 ) . map ( ( h ) = > h . id ) ) )
}
} )
2026-06-19 11:04:46 +00:00
}
function toggleReveal ( key : string ) {
setRevealed ( ( prev ) = > {
const next = new Set ( prev )
if ( next . has ( key ) ) next . delete ( key )
else next . add ( key )
return next
} )
}
function setBusyFlag ( id : number , value : boolean ) {
setBusy ( ( prev ) = > {
const next = new Set ( prev )
if ( value ) next . add ( id )
else next . delete ( id )
return next
} )
}
function addNewHost() {
const key = nextNewKey . current --
setNewDrafts ( ( prev ) = > [ . . . prev , { key , values : { } } ] )
}
function setNewDraftField ( key : number , fieldKey : string , value : string ) {
setNewDrafts ( ( prev ) = > prev . map ( ( d ) = > ( d . key === key ? { . . . d , values : { . . . d . values , [ fieldKey ] : value } } : d ) ) )
}
function removeNewDraft ( key : number ) {
setNewDrafts ( ( prev ) = > prev . filter ( ( d ) = > d . key !== key ) )
}
function setDraftField ( id : number , fieldKey : string , value : string ) {
setDrafts ( ( prev ) = > ( { . . . prev , [ id ] : { . . . prev [ id ] , [ fieldKey ] : value } } ) )
}
2026-06-19 11:12:33 +00:00
const fieldsWithJumpHost = ( ) : FieldDef [ ] = > [
2026-06-19 11:04:46 +00:00
. . . sshFields ,
{ key : 'jumpHostIntegrationId' , label : 'Jump Host (optional)' } ,
2026-06-19 11:28:51 +00:00
{ key : 'sessionLogging' , label : 'Record session to disk' } ,
2026-06-19 11:04:46 +00:00
]
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
function buildPayload ( fields : FieldDef [ ] , values : Record < string , string > , existing? : Integration ) {
const config : Record < string , string > = { . . . ( existing ? . config ? ? { } ) }
2026-06-19 11:04:46 +00:00
const secrets : Record < string , string > = { }
for ( const f of fields ) {
const value = values [ f . key ]
if ( value === undefined ) continue
if ( f . secret ) secrets [ f . key ] = value
else config [ f . key ] = value
}
return { config , secrets }
}
async function handleSaveExisting ( host : Integration ) {
setBusyFlag ( host . id , true )
setStatusMsg ( ( prev ) = > ( { . . . prev , [ host . id ] : '' } ) )
try {
const draft = drafts [ host . id ] ? ? { }
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
const { config , secrets } = buildPayload ( fieldsWithJumpHost ( ) , draft , host )
2026-06-20 07:23:22 -04:00
const name = draft . __name ? . trim ( )
const { integration } = await api . updateIntegration ( host . id , { . . . ( name ? { name } : { } ) , config , secrets } )
2026-06-19 11:04:46 +00:00
setHosts ( ( prev ) = > ( prev ? ? [ ] ) . map ( ( h ) = > ( h . id === integration . id ? integration : h ) ) )
setStatusMsg ( ( prev ) = > ( { . . . prev , [ host . id ] : 'Saved' } ) )
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
setCollapsed ( ( prev ) = > new Set ( prev ) . add ( host . id ) )
2026-06-19 11:04:46 +00:00
} catch ( err ) {
setStatusMsg ( ( prev ) = > ( { . . . prev , [ host . id ] : err instanceof ApiError ? err . message : 'Save failed' } ) )
} finally {
setBusyFlag ( host . id , false )
}
}
async function handleSaveNew ( key : number , values : Record < string , string > ) {
setBusyFlag ( key , true )
try {
const { config , secrets } = buildPayload ( fieldsWithJumpHost ( ) , values )
Add explicit Host Name field for SSH hosts (#14)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add explicit Host Name field for SSH hosts
The SSH host name was only editable via a small inline header input,
not a clearly labeled form field like the other options (Host / IP,
Port, etc). Add a proper "Host Name" labeled field to both existing
and new SSH host forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:48:22 -04:00
const name = values . __name ? . trim ( ) || ( values . host ? ` SSH: ${ values . host } ` : 'SSH Host' )
2026-06-19 11:04:46 +00:00
await api . createIntegration ( { type : 'ssh' , name , config , secrets } )
removeNewDraft ( key )
refresh ( )
} catch ( err ) {
setStatusMsg ( ( prev ) = > ( { . . . prev , [ key ] : err instanceof ApiError ? err . message : 'Save failed' } ) )
} finally {
setBusyFlag ( key , false )
}
}
async function handleTest ( host : Integration ) {
setBusyFlag ( host . id , true )
try {
const result = await api . testIntegration ( host . id )
setStatusMsg ( ( prev ) = > ( { . . . prev , [ host . id ] : result . message } ) )
refresh ( )
} catch ( err ) {
setStatusMsg ( ( prev ) = > ( { . . . prev , [ host . id ] : err instanceof ApiError ? err . message : 'Test failed' } ) )
} finally {
setBusyFlag ( host . id , false )
}
}
async function handleDelete ( host : Integration ) {
setBusyFlag ( host . id , true )
try {
await api . deleteIntegration ( host . id )
refresh ( )
} catch ( err ) {
setStatusMsg ( ( prev ) = > ( { . . . prev , [ host . id ] : err instanceof ApiError ? err . message : 'Delete failed' } ) )
setBusyFlag ( host . id , false )
}
}
function renderFields (
fields : FieldDef [ ] ,
values : Record < string , string > ,
onChange : ( fieldKey : string , value : string ) = > void ,
idForReveal : number ,
existing : Integration | undefined ,
excludeHostId? : number ,
) {
return fields . map ( ( f ) = > {
const key = ` ${ idForReveal } - ${ f . key } `
if ( f . key === 'jumpHostIntegrationId' ) {
const options = ( hosts ? ? [ ] ) . filter ( ( h ) = > h . id !== excludeHostId )
const savedValue = existing ? . config [ f . key ] ? ? ''
const value = values [ f . key ] ? ? savedValue
return (
< div key = { key } >
< label style = { labelStyle } > { f . label } < / label >
< select style = { inputStyle } value = { value } onChange = { ( e ) = > onChange ( f . key , e . target . value ) } >
< option value = "" > None < / option >
{ options . map ( ( h ) = > (
< option key = { h . id } value = { String ( h . id ) } >
{ h . name }
< / option >
) ) }
< / select >
< / div >
)
}
2026-06-19 11:28:51 +00:00
if ( f . key === 'sessionLogging' ) {
const savedValue = existing ? . config [ f . key ] === 'true'
const value = values [ f . key ] !== undefined ? values [ f . key ] === 'true' : savedValue
return (
< div key = { key } className = "flex items-end pb-1.5" >
< label className = "flex items-center gap-2 text-xs" style = { { color : '#E8E6E0' } } >
< input type = "checkbox" checked = { value } onChange = { ( e ) = > onChange ( f . key , e . target . checked ? 'true' : 'false' ) } / >
{ f . label }
< / label >
< / div >
)
}
2026-06-19 11:04:46 +00:00
const isRevealed = revealed . has ( key )
const savedValue = f . secret ? '' : existing ? . config [ f . key ] ? ? ''
const value = values [ f . key ] ? ? savedValue
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
if ( f . file ) {
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
const isSaved = existing ? . secretKeys ? . includes ( f . key )
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
return (
< div key = { key } className = "col-span-3" >
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
< label style = { labelStyle } >
{ f . label }
{ isSaved && < span style = { { color : '#2ECC71' , fontWeight : 400 } } > · saved < / span > }
< / label >
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
< div className = "relative" >
< textarea
style = { { . . . inputStyle , height : '90px' , paddingRight : '32px' , fontFamily : 'monospace' , resize : 'vertical' , whiteSpace : 'pre' } }
value = { value }
onChange = { ( e ) = > onChange ( f . key , e . target . value ) }
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
placeholder = { isSaved ? '•••••••••••• (saved — paste to replace)' : 'Paste key contents here, or upload a file' }
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
/ >
< input
type = "file"
ref = { ( el ) = > { fileInputRefs . current [ key ] = el } }
style = { { display : 'none' } }
onChange = { ( e ) = > {
const file = e . target . files ? . [ 0 ]
if ( ! file ) return
const reader = new FileReader ( )
reader . onload = ( ) = > onChange ( f . key , String ( reader . result ? ? '' ) . trim ( ) )
reader . readAsText ( file )
e . target . value = ''
} }
/ >
< button
onClick = { ( ) = > fileInputRefs . current [ key ] ? . click ( ) }
title = "Upload from file"
className = "absolute cursor-pointer border-none bg-transparent"
style = { { right : '8px' , top : '8px' , color : '#7A7D85' } }
>
< Upload size = { 13 } / >
< / button >
< / div >
< / div >
)
}
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
const isSavedSecret = f . secret && existing ? . secretKeys ? . includes ( f . key )
2026-06-19 11:04:46 +00:00
return (
< div key = { key } >
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
< label style = { labelStyle } >
{ f . label }
{ isSavedSecret && < span style = { { color : '#2ECC71' , fontWeight : 400 } } > · saved < / span > }
< / label >
2026-06-19 11:04:46 +00:00
< div className = "relative" >
< input
Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:11:32 -04:00
style = { { . . . inputStyle , paddingRight : f.secret ? '32px' : undefined } }
2026-06-19 11:04:46 +00:00
type = { f . secret && ! isRevealed ? 'password' : 'text' }
value = { value }
onChange = { ( e ) = > onChange ( f . key , e . target . value ) }
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
placeholder = { isSavedSecret ? '•••••••••••• (saved — type to replace)' : 'Not configured' }
2026-06-19 11:04:46 +00:00
/ >
{ f . secret && (
< button
onClick = { ( ) = > toggleReveal ( key ) }
className = "absolute cursor-pointer border-none bg-transparent"
style = { { right : '8px' , top : '50%' , transform : 'translateY(-50%)' , color : '#7A7D85' } }
>
{ isRevealed ? < EyeOff size = { 13 } / > : < Eye size = { 13 } / > }
< / button >
) }
< / div >
< / div >
)
} )
}
if ( ! hosts ) {
return (
< div style = { cardBase } >
< p style = { { fontSize : '12px' , color : '#7A7D85' } } > Loading SSH hosts … < / p >
< / div >
)
}
return (
< div className = "flex flex-col gap-4" >
{ hosts . map ( ( host ) = > {
const online = host . status === 'connected'
const draft = drafts [ host . id ] ? ? { }
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
const isCollapsed = collapsed . has ( host . id )
2026-06-19 11:04:46 +00:00
return (
< div key = { host . id } style = { cardBase } >
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
< div className = "flex items-center justify-between" style = { { marginBottom : isCollapsed ? 0 : '16px' } } >
< button
onClick = { ( ) = > toggleCollapsed ( host . id ) }
className = "flex items-center gap-2.5 cursor-pointer border-none bg-transparent"
style = { { padding : 0 } }
title = { isCollapsed ? 'Expand' : 'Collapse' }
>
{ isCollapsed ? < ChevronRight size = { 14 } color = "#7A7D85" / > : < ChevronDown size = { 14 } color = "#7A7D85" / > }
2026-06-19 11:04:46 +00:00
< span
style = { {
width : '8px' ,
height : '8px' ,
borderRadius : '50%' ,
backgroundColor : online ? '#2ECC71' : '#4A4D55' ,
boxShadow : online ? '0 0 6px rgba(46,204,113,0.6)' : 'none' ,
} }
/ >
Add explicit Host Name field for SSH hosts (#14)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add explicit Host Name field for SSH hosts
The SSH host name was only editable via a small inline header input,
not a clearly labeled form field like the other options (Host / IP,
Port, etc). Add a proper "Host Name" labeled field to both existing
and new SSH host forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:48:22 -04:00
< span style = { { fontSize : '13px' , color : '#E8E6E0' , fontWeight : 600 } } > { draft . __name ? ? host . name } < / span >
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
< / button >
2026-06-19 11:04:46 +00:00
< div className = "flex items-center gap-2" >
{ statusMsg [ host . id ] && < span style = { { fontSize : '11px' , color : '#7A7D85' } } > { statusMsg [ host . id ] } < / span > }
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
{ ! isCollapsed && (
< button
onClick = { ( ) = > handleSaveExisting ( host ) }
disabled = { busy . has ( host . id ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#0A0B0D' , backgroundColor : '#C8A434' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( host . id ) ? 0.6 : 1 } }
>
Save
< / button >
) }
2026-06-19 11:04:46 +00:00
< button
onClick = { ( ) = > handleTest ( host ) }
disabled = { busy . has ( host . id ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#C8A434' , backgroundColor : 'rgba(200,164,52,0.08)' , border : '1px solid rgba(200,164,52,0.2)' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( host . id ) ? 0.6 : 1 } }
>
Test Connection
< / button >
< button
onClick = { ( ) = > handleDelete ( host ) }
disabled = { busy . has ( host . id ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#E74C3C' , backgroundColor : 'transparent' , border : '1px solid rgba(231,76,60,0.3)' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( host . id ) ? 0.6 : 1 } }
>
< Trash2 size = { 12 } / >
< / button >
< / div >
< / div >
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
{ ! isCollapsed && (
< div className = "grid grid-cols-3 gap-4" style = { { marginTop : '16px' } } >
< div >
< label style = { labelStyle } > Host Name < / label >
< input
style = { inputStyle }
value = { draft . __name ? ? host . name }
onChange = { ( e ) = > setDraftField ( host . id , '__name' , e . target . value ) }
placeholder = "Not configured"
/ >
< / div >
{ renderFields ( fieldsWithJumpHost ( ) , draft , ( k , v ) = > setDraftField ( host . id , k , v ) , host . id , host , host . id ) }
Add explicit Host Name field for SSH hosts (#14)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add explicit Host Name field for SSH hosts
The SSH host name was only editable via a small inline header input,
not a clearly labeled form field like the other options (Host / IP,
Port, etc). Add a proper "Host Name" labeled field to both existing
and new SSH host forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:48:22 -04:00
< / div >
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
) }
2026-06-19 11:04:46 +00:00
< / div >
)
} ) }
{ newDrafts . map ( ( d ) = > (
< div key = { d . key } style = { cardBase } >
< div className = "flex items-center justify-between" style = { { marginBottom : '16px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' , fontWeight : 600 } } > New SSH Host < / span >
< div className = "flex items-center gap-2" >
{ statusMsg [ d . key ] && < span style = { { fontSize : '11px' , color : '#7A7D85' } } > { statusMsg [ d . key ] } < / span > }
< button
onClick = { ( ) = > handleSaveNew ( d . key , d . values ) }
disabled = { busy . has ( d . key ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#0A0B0D' , backgroundColor : '#C8A434' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( d . key ) ? 0.6 : 1 } }
>
Save
< / button >
< button
onClick = { ( ) = > removeNewDraft ( d . key ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#7A7D85' , backgroundColor : 'transparent' , border : '1px solid rgba(255,255,255,0.08)' , borderRadius : '6px' , padding : '6px 12px' } }
>
Cancel
< / button >
< / div >
< / div >
< div className = "grid grid-cols-3 gap-4" >
Add explicit Host Name field for SSH hosts (#14)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add explicit Host Name field for SSH hosts
The SSH host name was only editable via a small inline header input,
not a clearly labeled form field like the other options (Host / IP,
Port, etc). Add a proper "Host Name" labeled field to both existing
and new SSH host forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:48:22 -04:00
< div >
< label style = { labelStyle } > Host Name < / label >
< input
style = { inputStyle }
value = { d . values . __name ? ? '' }
onChange = { ( e ) = > setNewDraftField ( d . key , '__name' , e . target . value ) }
placeholder = "SSH Host"
/ >
< / div >
2026-06-19 11:04:46 +00:00
{ renderFields ( fieldsWithJumpHost ( ) , d . values , ( k , v ) = > setNewDraftField ( d . key , k , v ) , d . key , undefined ) }
< / div >
< / div >
) ) }
< button
onClick = { addNewHost }
className = "cursor-pointer border-none self-start"
style = { { fontSize : '12px' , fontWeight : 600 , color : '#C8A434' , backgroundColor : 'rgba(200,164,52,0.08)' , border : '1px solid rgba(200,164,52,0.2)' , borderRadius : '8px' , padding : '9px 16px' } }
>
+ Add SSH Host
< / button >
< / div >
)
}
2026-06-20 10:18:04 +00:00
type NewIntegrationDraft = { id : number ; type : string ; values : Record < string , string > }
2026-06-18 18:44:26 +00:00
function IntegrationsSection() {
2026-06-18 19:26:48 +00:00
const [ integrations , setIntegrations ] = useState < Integration [ ] | null > ( null )
2026-06-18 18:44:26 +00:00
const [ revealed , setRevealed ] = useState < Set < string > > ( new Set ( ) )
2026-06-20 10:18:04 +00:00
const [ editDrafts , setEditDrafts ] = useState < Record < number , Record < string , string > > > ( { } )
const [ newDrafts , setNewDrafts ] = useState < NewIntegrationDraft [ ] > ( [ ] )
2026-06-18 19:26:48 +00:00
const [ statusMsg , setStatusMsg ] = useState < Record < string , string > > ( { } )
const [ busy , setBusy ] = useState < Set < string > > ( new Set ( ) )
2026-06-20 10:18:04 +00:00
const nextDraftId = useRef ( - 1 )
2026-06-18 19:26:48 +00:00
useEffect ( ( ) = > {
api . listIntegrations ( ) . then ( ( { integrations } ) = > setIntegrations ( integrations ) )
} , [ ] )
2026-06-18 18:44:26 +00:00
function toggleReveal ( key : string ) {
setRevealed ( ( prev ) = > {
const next = new Set ( prev )
if ( next . has ( key ) ) next . delete ( key )
else next . add ( key )
return next
} )
}
2026-06-20 10:18:04 +00:00
function setBusyFlag ( rowKey : string , value : boolean ) {
2026-06-18 19:26:48 +00:00
setBusy ( ( prev ) = > {
const next = new Set ( prev )
2026-06-20 10:18:04 +00:00
if ( value ) next . add ( rowKey )
else next . delete ( rowKey )
2026-06-18 19:26:48 +00:00
return next
} )
}
2026-06-20 10:18:04 +00:00
function setEditField ( integrationId : number , fieldKey : string , value : string ) {
setEditDrafts ( ( prev ) = > ( { . . . prev , [ integrationId ] : { . . . prev [ integrationId ] , [ fieldKey ] : value } } ) )
}
function setNewDraftField ( draftId : number , fieldKey : string , value : string ) {
setNewDrafts ( ( prev ) = > prev . map ( ( d ) = > ( d . id === draftId ? { . . . d , values : { . . . d . values , [ fieldKey ] : value } } : d ) ) )
}
function addNewDraft ( type : string ) {
const id = nextDraftId . current --
setNewDrafts ( ( prev ) = > [ . . . prev , { id , type , values : { } } ] )
}
function removeNewDraft ( id : number ) {
setNewDrafts ( ( prev ) = > prev . filter ( ( d ) = > d . id !== id ) )
}
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
function buildPayload ( def : ( typeof integrationTypeDefs ) [ number ] , values : Record < string , string > , existing? : Integration ) {
const config : Record < string , string > = { . . . ( existing ? . config ? ? { } ) }
2026-06-20 10:18:04 +00:00
const secrets : Record < string , string > = { }
for ( const f of def . fields ) {
const value = values [ f . key ]
if ( value === undefined ) continue
if ( f . secret ) secrets [ f . key ] = value
else config [ f . key ] = value
}
return { config , secrets }
2026-06-18 19:26:48 +00:00
}
2026-06-20 10:18:04 +00:00
async function handleSaveExisting ( def : ( typeof integrationTypeDefs ) [ number ] , existing : Integration ) {
const rowKey = ` e- ${ existing . id } `
setBusyFlag ( rowKey , true )
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : '' } ) )
2026-06-18 19:26:48 +00:00
try {
Fix integration save data loss; add SSH host card collapse (#16)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Fix integration save wiping untouched config fields
The PUT /api/integrations/:id route fully overwrites config_json with
whatever config object is sent (no merge), but buildPayload only
included fields the user had actually edited. Saving after editing
just one field (e.g. pasting a new SSH key) silently dropped every
other config field. Merge the existing integration's config into the
payload before sending.
* Add collapse/expand for SSH host cards
Click the chevron to collapse a host's card once it's configured.
Collapsed cards keep all field state in memory (just hidden), and
auto-collapse after a successful Save.
* Install openssh-client in backend image for certificate-auth SSH
Certificate-based SSH connections shell out to the system ssh binary
via node-pty (ssh2 has no OpenSSH certificate support), but the
alpine runtime image never installed openssh-client. This caused
'execvp(3) failed: No such file or directory' for any host with an
OPKSSH certificate configured.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:30:21 -04:00
const { config , secrets } = buildPayload ( def , editDrafts [ existing . id ] ? ? { } , existing )
Add editable name field to generic integrations (#13)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:40:03 -04:00
const name = editDrafts [ existing . id ] ? . __name ? . trim ( )
const { integration } = await api . updateIntegration ( existing . id , { . . . ( name ? { name } : { } ) , config , secrets } )
2026-06-20 10:18:04 +00:00
setIntegrations ( ( prev ) = > ( prev ? ? [ ] ) . map ( ( i ) = > ( i . id === integration . id ? integration : i ) ) )
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : 'Saved' } ) )
2026-06-18 19:26:48 +00:00
} catch ( err ) {
2026-06-20 10:18:04 +00:00
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : err instanceof ApiError ? err . message : 'Save failed' } ) )
2026-06-18 19:26:48 +00:00
} finally {
2026-06-20 10:18:04 +00:00
setBusyFlag ( rowKey , false )
2026-06-18 19:26:48 +00:00
}
}
2026-06-20 10:18:04 +00:00
async function handleSaveNew ( def : ( typeof integrationTypeDefs ) [ number ] , draft : NewIntegrationDraft ) {
const rowKey = ` n- ${ draft . id } `
setBusyFlag ( rowKey , true )
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : '' } ) )
try {
const { config , secrets } = buildPayload ( def , draft . values )
Add editable name field to generic integrations (#13)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:40:03 -04:00
const name = draft . values . __name ? . trim ( ) || def . name
const { integration } = await api . createIntegration ( { type : def . type , name , config , secrets } )
2026-06-20 10:18:04 +00:00
setIntegrations ( ( prev ) = > [ . . . ( prev ? ? [ ] ) , integration ] )
removeNewDraft ( draft . id )
} catch ( err ) {
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : err instanceof ApiError ? err . message : 'Save failed' } ) )
} finally {
setBusyFlag ( rowKey , false )
2026-06-18 19:26:48 +00:00
}
2026-06-20 10:18:04 +00:00
}
async function handleTest ( existing : Integration ) {
const rowKey = ` e- ${ existing . id } `
setBusyFlag ( rowKey , true )
2026-06-18 19:26:48 +00:00
try {
const result = await api . testIntegration ( existing . id )
2026-06-20 10:18:04 +00:00
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : result . message } ) )
2026-06-18 19:26:48 +00:00
const { integrations } = await api . listIntegrations ( )
setIntegrations ( integrations )
} catch ( err ) {
2026-06-20 10:18:04 +00:00
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : err instanceof ApiError ? err . message : 'Test failed' } ) )
2026-06-18 19:26:48 +00:00
} finally {
2026-06-20 10:18:04 +00:00
setBusyFlag ( rowKey , false )
}
}
async function handleDelete ( existing : Integration ) {
if ( ! window . confirm ( ` Remove this ${ existing . name } integration? ` ) ) return
const rowKey = ` e- ${ existing . id } `
setBusyFlag ( rowKey , true )
try {
await api . deleteIntegration ( existing . id )
setIntegrations ( ( prev ) = > ( prev ? ? [ ] ) . filter ( ( i ) = > i . id !== existing . id ) )
} catch ( err ) {
setStatusMsg ( ( prev ) = > ( { . . . prev , [ rowKey ] : err instanceof ApiError ? err . message : 'Delete failed' } ) )
setBusyFlag ( rowKey , false )
2026-06-18 19:26:48 +00:00
}
}
if ( ! integrations ) {
return (
< div style = { cardBase } >
< p style = { { fontSize : '12px' , color : '#7A7D85' } } > Loading integrations … < / p >
< / div >
)
}
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
function renderFields ( def : ( typeof integrationTypeDefs ) [ number ] , rowKey : string , getValue : ( f : FieldDef ) = > string , onChange : ( f : FieldDef , value : string ) = > void , placeholderForSecret : string , existing? : Integration ) {
2026-06-20 10:18:04 +00:00
return (
< div className = "grid grid-cols-3 gap-4" >
{ def . fields . map ( ( f ) = > {
const key = ` ${ rowKey } - ${ f . key } `
const isRevealed = revealed . has ( key )
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
const isSavedSecret = f . secret && existing ? . secretKeys ? . includes ( f . key )
2026-06-20 10:18:04 +00:00
return (
< div key = { key } >
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
< label style = { labelStyle } >
{ f . label }
{ isSavedSecret && < span style = { { color : '#2ECC71' , fontWeight : 400 } } > · saved < / span > }
< / label >
2026-06-20 10:18:04 +00:00
< div className = "relative" >
< input
style = { inputStyle }
type = { f . secret && ! isRevealed ? 'password' : 'text' }
value = { getValue ( f ) }
onChange = { ( e ) = > onChange ( f , e . target . value ) }
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
placeholder = { f . secret ? ( isSavedSecret ? ` ${ placeholderForSecret } (saved — type to replace) ` : placeholderForSecret ) : f . placeholder ? ? 'Not configured' }
2026-06-20 10:18:04 +00:00
/ >
{ f . secret && (
< button
onClick = { ( ) = > toggleReveal ( key ) }
className = "absolute cursor-pointer border-none bg-transparent"
style = { { right : '8px' , top : '50%' , transform : 'translateY(-50%)' , color : '#7A7D85' } }
>
{ isRevealed ? < EyeOff size = { 13 } / > : < Eye size = { 13 } / > }
< / button >
) }
< / div >
{ f . hint && (
< p style = { { fontSize : '10.5px' , color : '#7A7D85' , marginTop : '4px' , lineHeight : 1.4 , maxWidth : '260px' } } > { f . hint } < / p >
) }
< / div >
)
} ) }
< / div >
)
}
2026-06-18 18:44:26 +00:00
return (
< div className = "flex flex-col gap-4" >
2026-06-19 11:04:46 +00:00
< div >
< h3 style = { sectionTitle } > SSH Hosts < / h3 >
< SshHostsSection / >
< / div >
< div >
< h3 style = { sectionTitle } > Other Integrations < / h3 >
< / div >
2026-06-18 19:26:48 +00:00
{ integrationTypeDefs . map ( ( def ) = > {
2026-06-20 10:18:04 +00:00
const existingRows = integrations . filter ( ( i ) = > i . type === def . type )
const draftRows = newDrafts . filter ( ( d ) = > d . type === def . type )
const canAddAnother = def . multiInstance || existingRows . length + draftRows . length === 0
2026-06-18 19:26:48 +00:00
return (
2026-06-20 10:18:04 +00:00
< div key = { def . type } className = "flex flex-col gap-3" >
{ existingRows . map ( ( existing ) = > {
const rowKey = ` e- ${ existing . id } `
const online = existing . status === 'connected'
const draft = editDrafts [ existing . id ] ? ? { }
return (
< div key = { existing . id } style = { cardBase } >
< div className = "flex items-center justify-between" style = { { marginBottom : '16px' } } >
< div className = "flex items-center gap-2.5" >
< span
style = { {
width : '8px' ,
height : '8px' ,
borderRadius : '50%' ,
backgroundColor : online ? '#2ECC71' : '#4A4D55' ,
boxShadow : online ? '0 0 6px rgba(46,204,113,0.6)' : 'none' ,
} }
2026-06-18 19:26:48 +00:00
/ >
Add editable name field to generic integrations (#13)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:40:03 -04:00
< input
value = { draft . __name ? ? existing . name ? ? def . name }
onChange = { ( e ) = > setEditField ( existing . id , '__name' , e . target . value ) }
style = { { fontSize : '13px' , color : '#E8E6E0' , fontWeight : 600 , backgroundColor : 'transparent' , border : 'none' , outline : 'none' , padding : 0 , width : '160px' } }
/ >
2026-06-20 10:18:04 +00:00
{ existing . config . baseUrl || existing . config . hostname ? (
< span style = { { fontSize : '11px' , color : '#7A7D85' } } > { existing . config . baseUrl || existing . config . hostname } < / span >
) : null }
< / div >
< div className = "flex items-center gap-2" >
{ statusMsg [ rowKey ] && (
< span style = { { fontSize : '11px' , color : '#7A7D85' } } > { statusMsg [ rowKey ] } < / span >
2026-06-18 19:26:48 +00:00
) }
2026-06-20 10:18:04 +00:00
< button
onClick = { ( ) = > handleSaveExisting ( def , existing ) }
disabled = { busy . has ( rowKey ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#0A0B0D' , backgroundColor : '#C8A434' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( rowKey ) ? 0.6 : 1 } }
>
Save
< / button >
< button
onClick = { ( ) = > handleTest ( existing ) }
disabled = { busy . has ( rowKey ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#C8A434' , backgroundColor : 'rgba(200,164,52,0.08)' , border : '1px solid rgba(200,164,52,0.2)' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( rowKey ) ? 0.6 : 1 } }
>
Test Connection
< / button >
< button
onClick = { ( ) = > handleDelete ( existing ) }
disabled = { busy . has ( rowKey ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#E74C3C' , backgroundColor : 'rgba(231,76,60,0.08)' , border : '1px solid rgba(231,76,60,0.2)' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( rowKey ) ? 0.6 : 1 } }
>
Remove
< / button >
2026-06-18 19:26:48 +00:00
< / div >
2026-06-18 18:44:26 +00:00
< / div >
2026-06-20 10:18:04 +00:00
{ renderFields (
def ,
rowKey ,
( f ) = > draft [ f . key ] ? ? ( f . secret ? '' : existing . config [ f . key ] ? ? '' ) ,
( f , value ) = > setEditField ( existing . id , f . key , value ) ,
'••••••••••••' ,
Show saved indicator for secret fields instead of appearing deleted (#18)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Show saved indicator for secret fields instead of appearing blank/deleted
GET /api/integrations never returns decrypted secret values (by design),
so after navigating away and back, secret/key fields rendered empty -
looking exactly like the saved key had been deleted, even though it was
still intact and encrypted in the database. Expose which secret keys
exist (names only, never values) via secretKeys, and use it to label
fields as "saved" with an appropriate placeholder instead of blank.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 08:53:56 -04:00
existing ,
2026-06-20 10:18:04 +00:00
) }
< / div >
)
} ) }
{ draftRows . map ( ( draft ) = > {
const rowKey = ` n- ${ draft . id } `
return (
< div key = { draft . id } style = { cardBase } >
< div className = "flex items-center justify-between" style = { { marginBottom : '16px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' , fontWeight : 600 } } > New { def . name } < / span >
< div className = "flex items-center gap-2" >
{ statusMsg [ rowKey ] && (
< span style = { { fontSize : '11px' , color : '#7A7D85' } } > { statusMsg [ rowKey ] } < / span >
) }
< button
onClick = { ( ) = > handleSaveNew ( def , draft ) }
disabled = { busy . has ( rowKey ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#0A0B0D' , backgroundColor : '#C8A434' , borderRadius : '6px' , padding : '6px 12px' , opacity : busy.has ( rowKey ) ? 0.6 : 1 } }
>
Save
< / button >
< button
onClick = { ( ) = > removeNewDraft ( draft . id ) }
className = "cursor-pointer border-none"
style = { { fontSize : '11px' , fontWeight : 600 , color : '#7A7D85' , backgroundColor : 'rgba(255,255,255,0.04)' , border : '1px solid rgba(255,255,255,0.1)' , borderRadius : '6px' , padding : '6px 12px' } }
>
Cancel
< / button >
< / div >
< / div >
Add editable name field to generic integrations (#13)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 07:40:03 -04:00
< div className = "grid grid-cols-3 gap-4" style = { { marginBottom : '16px' } } >
< div >
< label style = { labelStyle } > Name < / label >
< input
style = { inputStyle }
value = { draft . values . __name ? ? '' }
onChange = { ( e ) = > setNewDraftField ( draft . id , '__name' , e . target . value ) }
placeholder = { def . name }
/ >
< / div >
< / div >
2026-06-20 10:18:04 +00:00
{ renderFields (
def ,
rowKey ,
( f ) = > draft . values [ f . key ] ? ? '' ,
( f , value ) = > setNewDraftField ( draft . id , f . key , value ) ,
'' ,
) }
< / div >
)
} ) }
{ existingRows . length === 0 && draftRows . length === 0 && (
< button
onClick = { ( ) = > addNewDraft ( def . type ) }
className = "cursor-pointer self-start"
style = { { fontSize : '12px' , fontWeight : 600 , color : '#C8A434' , backgroundColor : 'rgba(200,164,52,0.08)' , border : '1px solid rgba(200,164,52,0.2)' , borderRadius : '8px' , padding : '9px 16px' } }
>
+ Add { def . name }
< / button >
) }
{ canAddAnother && ( existingRows . length > 0 || draftRows . length > 0 ) && (
< button
onClick = { ( ) = > addNewDraft ( def . type ) }
className = "cursor-pointer self-start"
style = { { fontSize : '12px' , fontWeight : 600 , color : '#C8A434' , backgroundColor : 'rgba(200,164,52,0.08)' , border : '1px solid rgba(200,164,52,0.2)' , borderRadius : '8px' , padding : '9px 16px' } }
>
+ Add Another { def . name }
< / button >
) }
2026-06-18 18:44:26 +00:00
< / div >
2026-06-18 19:26:48 +00:00
)
} ) }
2026-06-18 18:44:26 +00:00
< / div >
)
}
function NotificationsSection() {
const [ enabled , setEnabled ] = useState ( true )
const [ email , setEmail ] = useState ( true )
const [ push , setPush ] = useState ( false )
const [ sound , setSound ] = useState ( true )
return (
< div style = { cardBase } >
< h3 style = { sectionTitle } > Notifications < / h3 >
< div className = "flex items-center justify-between" style = { { marginBottom : '20px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Enable Notifications < / span >
< Toggle on = { enabled } onClick = { ( ) = > setEnabled ( ( v ) = > ! v ) } / >
< / div >
< div style = { { marginBottom : '20px' } } >
< label style = { labelStyle } > Alert Threshold < / label >
< select style = { { . . . inputStyle , width : '220px' } } defaultValue = "all" >
< option value = "all" > All < / option >
< option value = "critical" > Critical Only < / option >
< option value = "warning" > Warning & Above < / option >
< / select >
< / div >
< div className = "flex items-center justify-between" style = { { marginBottom : '16px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Email Notifications < / span >
< Toggle on = { email } onClick = { ( ) = > setEmail ( ( v ) = > ! v ) } / >
< / div >
< div className = "flex items-center justify-between" style = { { marginBottom : '16px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Browser Push < / span >
< Toggle on = { push } onClick = { ( ) = > setPush ( ( v ) = > ! v ) } / >
< / div >
< div className = "flex items-center justify-between" style = { { marginBottom : '8px' } } >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Sound < / span >
< Toggle on = { sound } onClick = { ( ) = > setSound ( ( v ) = > ! v ) } / >
< / div >
{ sound && (
< input type = "range" min = { 0 } max = { 100 } defaultValue = { 70 } className = "w-full" style = { { accentColor : '#C8A434' } } / >
) }
< / div >
)
}
function DataBackupSection() {
2026-06-19 16:13:29 +00:00
const [ busy , setBusy ] = useState ( false )
const [ message , setMessage ] = useState < string | null > ( null )
const [ error , setError ] = useState < string | null > ( null )
const importRef = useRef < HTMLInputElement | null > ( null )
async function handleExport() {
setBusy ( true )
setError ( null )
setMessage ( null )
try {
const data = await api . exportData ( )
const blob = new Blob ( [ JSON . stringify ( data , null , 2 ) ] , { type : 'application/json' } )
const url = URL . createObjectURL ( blob )
const a = document . createElement ( 'a' )
a . href = url
a . download = ` archnest-backup- ${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .json `
a . click ( )
URL . revokeObjectURL ( url )
setMessage ( ` Exported ${ data . integrations . length } integrations, ${ data . bookmarks . length } bookmarks, ${ data . tunnels . length } tunnels. ` )
} catch ( err ) {
setError ( err instanceof Error ? err . message : 'Export failed' )
} finally {
setBusy ( false )
}
}
async function handleImportFile ( file : File ) {
setBusy ( true )
setError ( null )
setMessage ( null )
try {
const text = await file . text ( )
const parsed = JSON . parse ( text )
const result = await api . importData ( parsed )
const c = result . imported
setMessage ( ` Imported ${ c . integrations } integrations, ${ c . bookmarkCategories } categories, ${ c . bookmarks } bookmarks, ${ c . tunnels } tunnels. ` )
} catch ( err ) {
setError ( err instanceof Error ? err . message : 'Import failed — is this a valid ArchNest backup file?' )
} finally {
setBusy ( false )
}
}
2026-06-18 18:44:26 +00:00
return (
< div style = { cardBase } >
< h3 style = { sectionTitle } > Data & Backup < / h3 >
2026-06-19 16:13:29 +00:00
< p style = { { fontSize : '12px' , color : '#7A7D85' , marginBottom : '16px' , maxWidth : '460px' } } >
Export a portable backup of all integrations ( including their credentials ) , bookmarks , and tunnels as a single JSON file , or
import one into this instance . Imports are additive — existing data is kept and the backup ' s items are added alongside it .
The backup contains plaintext credentials , so store it securely .
< / p >
< div className = "flex flex-col gap-3" style = { { maxWidth : '460px' } } >
2026-06-18 18:44:26 +00:00
< div className = "flex items-center justify-between" >
2026-06-19 16:13:29 +00:00
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Export all data ( JSON ) < / span >
< GoldButton onClick = { handleExport } disabled = { busy } >
< Download size = { 13 } / > Export
< / GoldButton >
2026-06-18 18:44:26 +00:00
< / div >
< div className = "flex items-center justify-between" >
2026-06-19 16:13:29 +00:00
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } > Import from backup ( JSON ) < / span >
< GoldButton onClick = { ( ) = > importRef . current ? . click ( ) } disabled = { busy } >
< Upload size = { 13 } / > Import
< / GoldButton >
< input
ref = { importRef }
type = "file"
accept = "application/json,.json"
className = "hidden"
onChange = { ( e ) = > {
const file = e . target . files ? . [ 0 ]
if ( file ) handleImportFile ( file )
e . target . value = ''
} }
/ >
2026-06-18 18:44:26 +00:00
< / div >
2026-06-19 16:13:29 +00:00
{ message && < p style = { { fontSize : '12px' , color : '#2ECC71' } } > { message } < / p > }
{ error && < p style = { { fontSize : '12px' , color : '#E74C3C' } } > { error } < / p > }
2026-06-18 18:44:26 +00:00
< / div >
< / div >
)
}
function AboutSection() {
const rows : [ string , string ] [ ] = [
[ 'App' , 'ArchNest Dashboard v1.0.0' ] ,
[ 'Author' , 'Samuel James' ] ,
[ 'Repo' , 'github.com/SamuelSJames/archnest' ] ,
[ 'Stack' , 'React 19, Vite, TypeScript' ] ,
[ 'License' , 'MIT' ] ,
]
return (
< div style = { cardBase } >
< h3 style = { sectionTitle } > About < / h3 >
< div className = "flex flex-col gap-3" >
{ rows . map ( ( [ label , value ] ) = > (
< div key = { label } className = "flex items-center justify-between" >
< span style = { { fontSize : '12px' , color : '#7A7D85' } } > { label } < / span >
< span style = { { fontSize : '12px' , color : '#E8E6E0' } } > { value } < / span >
< / div >
) ) }
< / div >
< / div >
)
}
Add auth Phase 2: password change, sessions, and login audit log (#27)
Builds out the Settings → Security tab (previously a "coming soon"
placeholder) and the backend behind it. Still single-user; multi-user
and SSO remain Phases 3-4.
Backend:
- New `sessions` table (id, user_id, user_agent, ip, created_at,
last_seen_at) and `login_events` table (user_id, username, ip,
user_agent, success, created_at).
- Login and setup now mint a session row and embed its id as a `sid`
claim in the JWT. The `authenticate` hook validates that the session
still exists (and bumps last_seen_at), so revoking a session genuinely
invalidates its token instead of relying on the JWT signature alone.
Tokens minted before sessions existed have no `sid` and stay valid
until expiry, for backward compatibility.
- Every login attempt (success and failure) is recorded in login_events
for the audit trail.
- New endpoints: PUT /api/auth/password (verifies current via bcrypt,
hashes new at cost 12, revokes all *other* sessions on success),
GET /api/auth/sessions, DELETE /api/auth/sessions/:id (can't revoke
the current one), POST /api/auth/logout (revokes current session),
GET /api/auth/login-events?limit.
- AuthContext.logout() now calls POST /api/auth/logout best-effort so
signing out revokes the server session, not just the local token.
Frontend:
- SecuritySection: change-password form (current/new/confirm with
show/hide and client-side validation), active-sessions list (device
description from user-agent, IP, last-seen relative time, per-session
"Sign out" for non-current sessions), and a recent login-activity feed
(success/failure dot, user, IP, relative time).
- api.ts: changePassword/listSessions/revokeSession/logout/
listLoginEvents + AuthSession/LoginEvent types.
Verified end-to-end against a throwaway backend instance: session
creation, second-device session, failed-login logging, cross-session
revocation invalidating the revoked token, password change keeping the
current session alive while revoking others, and logout invalidating the
current session. Frontend + backend both type-check clean.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
2026-06-20 11:50:56 -04:00
function relativeTime ( iso : string ) : string {
// SQLite datetime('now') returns UTC without a timezone marker; treat it as UTC.
const ts = Date . parse ( iso . includes ( 'T' ) ? iso : iso.replace ( ' ' , 'T' ) + 'Z' )
if ( Number . isNaN ( ts ) ) return iso
const diffMs = Date . now ( ) - ts
const sec = Math . round ( diffMs / 1000 )
if ( sec < 60 ) return 'just now'
const min = Math . round ( sec / 60 )
if ( min < 60 ) return ` ${ min } m ago `
const hr = Math . round ( min / 60 )
if ( hr < 24 ) return ` ${ hr } h ago `
const day = Math . round ( hr / 24 )
if ( day < 30 ) return ` ${ day } d ago `
return new Date ( ts ) . toLocaleDateString ( )
}
function describeUserAgent ( ua : string | null ) : string {
if ( ! ua ) return 'Unknown device'
let os = 'Unknown OS'
if ( /Windows/i . test ( ua ) ) os = 'Windows'
else if ( /Macintosh|Mac OS/i . test ( ua ) ) os = 'macOS'
else if ( /Android/i . test ( ua ) ) os = 'Android'
else if ( /iPhone|iPad|iOS/i . test ( ua ) ) os = 'iOS'
else if ( /Linux/i . test ( ua ) ) os = 'Linux'
let browser = ''
if ( /Edg\//i . test ( ua ) ) browser = 'Edge'
else if ( /Chrome\//i . test ( ua ) && ! /Chromium/i . test ( ua ) ) browser = 'Chrome'
else if ( /Firefox\//i . test ( ua ) ) browser = 'Firefox'
else if ( /Safari\//i . test ( ua ) && ! /Chrome/i . test ( ua ) ) browser = 'Safari'
return browser ? ` ${ browser } on ${ os } ` : os
}
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
function SecuritySection() {
Add auth Phase 2: password change, sessions, and login audit log (#27)
Builds out the Settings → Security tab (previously a "coming soon"
placeholder) and the backend behind it. Still single-user; multi-user
and SSO remain Phases 3-4.
Backend:
- New `sessions` table (id, user_id, user_agent, ip, created_at,
last_seen_at) and `login_events` table (user_id, username, ip,
user_agent, success, created_at).
- Login and setup now mint a session row and embed its id as a `sid`
claim in the JWT. The `authenticate` hook validates that the session
still exists (and bumps last_seen_at), so revoking a session genuinely
invalidates its token instead of relying on the JWT signature alone.
Tokens minted before sessions existed have no `sid` and stay valid
until expiry, for backward compatibility.
- Every login attempt (success and failure) is recorded in login_events
for the audit trail.
- New endpoints: PUT /api/auth/password (verifies current via bcrypt,
hashes new at cost 12, revokes all *other* sessions on success),
GET /api/auth/sessions, DELETE /api/auth/sessions/:id (can't revoke
the current one), POST /api/auth/logout (revokes current session),
GET /api/auth/login-events?limit.
- AuthContext.logout() now calls POST /api/auth/logout best-effort so
signing out revokes the server session, not just the local token.
Frontend:
- SecuritySection: change-password form (current/new/confirm with
show/hide and client-side validation), active-sessions list (device
description from user-agent, IP, last-seen relative time, per-session
"Sign out" for non-current sessions), and a recent login-activity feed
(success/failure dot, user, IP, relative time).
- api.ts: changePassword/listSessions/revokeSession/logout/
listLoginEvents + AuthSession/LoginEvent types.
Verified end-to-end against a throwaway backend instance: session
creation, second-device session, failed-login logging, cross-session
revocation invalidating the revoked token, password change keeping the
current session alive while revoking others, and logout invalidating the
current session. Frontend + backend both type-check clean.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
2026-06-20 11:50:56 -04:00
// Change-password form
const [ currentPassword , setCurrentPassword ] = useState ( '' )
const [ newPassword , setNewPassword ] = useState ( '' )
const [ confirmPassword , setConfirmPassword ] = useState ( '' )
const [ showCurrent , setShowCurrent ] = useState ( false )
const [ showNew , setShowNew ] = useState ( false )
const [ changing , setChanging ] = useState ( false )
const [ pwMsg , setPwMsg ] = useState < { text : string ; ok : boolean } | null > ( null )
// Sessions + login events
const [ sessions , setSessions ] = useState < AuthSession [ ] > ( [ ] )
const [ events , setEvents ] = useState < LoginEvent [ ] > ( [ ] )
const [ loading , setLoading ] = useState ( true )
async function loadActivity() {
try {
const [ s , e ] = await Promise . all ( [ api . listSessions ( ) , api . listLoginEvents ( 15 ) ] )
setSessions ( s . sessions )
setEvents ( e . events )
} catch {
// leave existing data on transient failure
} finally {
setLoading ( false )
}
}
useEffect ( ( ) = > {
loadActivity ( )
} , [ ] )
async function handleChangePassword() {
setPwMsg ( null )
if ( newPassword . length < 8 ) {
setPwMsg ( { text : 'New password must be at least 8 characters' , ok : false } )
return
}
if ( newPassword !== confirmPassword ) {
setPwMsg ( { text : 'New passwords do not match' , ok : false } )
return
}
setChanging ( true )
try {
await api . changePassword ( currentPassword , newPassword )
setPwMsg ( { text : 'Password changed. Other sessions were signed out.' , ok : true } )
setCurrentPassword ( '' )
setNewPassword ( '' )
setConfirmPassword ( '' )
loadActivity ( )
} catch ( err ) {
setPwMsg ( { text : err instanceof ApiError ? err . message : 'Failed to change password' , ok : false } )
} finally {
setChanging ( false )
}
}
async function handleRevoke ( id : string ) {
try {
await api . revokeSession ( id )
setSessions ( ( prev ) = > prev . filter ( ( s ) = > s . id !== id ) )
} catch {
loadActivity ( )
}
}
const pwInputWrap : React.CSSProperties = { position : 'relative' }
const eyeBtn : React.CSSProperties = {
position : 'absolute' ,
right : '10px' ,
top : '50%' ,
transform : 'translateY(-50%)' ,
background : 'none' ,
border : 'none' ,
cursor : 'pointer' ,
color : '#7A7D85' ,
display : 'flex' ,
}
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
return (
Add auth Phase 2: password change, sessions, and login audit log (#27)
Builds out the Settings → Security tab (previously a "coming soon"
placeholder) and the backend behind it. Still single-user; multi-user
and SSO remain Phases 3-4.
Backend:
- New `sessions` table (id, user_id, user_agent, ip, created_at,
last_seen_at) and `login_events` table (user_id, username, ip,
user_agent, success, created_at).
- Login and setup now mint a session row and embed its id as a `sid`
claim in the JWT. The `authenticate` hook validates that the session
still exists (and bumps last_seen_at), so revoking a session genuinely
invalidates its token instead of relying on the JWT signature alone.
Tokens minted before sessions existed have no `sid` and stay valid
until expiry, for backward compatibility.
- Every login attempt (success and failure) is recorded in login_events
for the audit trail.
- New endpoints: PUT /api/auth/password (verifies current via bcrypt,
hashes new at cost 12, revokes all *other* sessions on success),
GET /api/auth/sessions, DELETE /api/auth/sessions/:id (can't revoke
the current one), POST /api/auth/logout (revokes current session),
GET /api/auth/login-events?limit.
- AuthContext.logout() now calls POST /api/auth/logout best-effort so
signing out revokes the server session, not just the local token.
Frontend:
- SecuritySection: change-password form (current/new/confirm with
show/hide and client-side validation), active-sessions list (device
description from user-agent, IP, last-seen relative time, per-session
"Sign out" for non-current sessions), and a recent login-activity feed
(success/failure dot, user, IP, relative time).
- api.ts: changePassword/listSessions/revokeSession/logout/
listLoginEvents + AuthSession/LoginEvent types.
Verified end-to-end against a throwaway backend instance: session
creation, second-device session, failed-login logging, cross-session
revocation invalidating the revoked token, password change keeping the
current session alive while revoking others, and logout invalidating the
current session. Frontend + backend both type-check clean.
Co-authored-by: Samuel James <ssamjame@amazon.com>
Co-authored-by: Kiro <noreply@kiro.dev>
2026-06-20 11:50:56 -04:00
< div className = "flex flex-col gap-5" >
{ /* Change password */ }
< div style = { cardBase } >
< h3 style = { sectionTitle } > Change Password < / h3 >
< div className = "flex flex-col gap-4" style = { { maxWidth : '420px' } } >
< div >
< label style = { labelStyle } > Current Password < / label >
< div style = { pwInputWrap } >
< input
style = { { . . . inputStyle , paddingRight : '38px' } }
type = { showCurrent ? 'text' : 'password' }
autoComplete = "current-password"
value = { currentPassword }
onChange = { ( e ) = > setCurrentPassword ( e . target . value ) }
/ >
< button style = { eyeBtn } onClick = { ( ) = > setShowCurrent ( ( v ) = > ! v ) } type = "button" title = { showCurrent ? 'Hide' : 'Show' } >
{ showCurrent ? < EyeOff size = { 15 } / > : < Eye size = { 15 } / > }
< / button >
< / div >
< / div >
< div >
< label style = { labelStyle } > New Password < / label >
< div style = { pwInputWrap } >
< input
style = { { . . . inputStyle , paddingRight : '38px' } }
type = { showNew ? 'text' : 'password' }
autoComplete = "new-password"
value = { newPassword }
onChange = { ( e ) = > setNewPassword ( e . target . value ) }
/ >
< button style = { eyeBtn } onClick = { ( ) = > setShowNew ( ( v ) = > ! v ) } type = "button" title = { showNew ? 'Hide' : 'Show' } >
{ showNew ? < EyeOff size = { 15 } / > : < Eye size = { 15 } / > }
< / button >
< / div >
< / div >
< div >
< label style = { labelStyle } > Confirm New Password < / label >
< input
style = { inputStyle }
type = { showNew ? 'text' : 'password' }
autoComplete = "new-password"
value = { confirmPassword }
onChange = { ( e ) = > setConfirmPassword ( e . target . value ) }
/ >
< / div >
< div className = "flex items-center gap-3" >
< GoldButton
onClick = { handleChangePassword }
disabled = { changing || ! currentPassword || ! newPassword || ! confirmPassword }
>
{ changing ? 'Saving…' : 'Update Password' }
< / GoldButton >
{ pwMsg && (
< span style = { { fontSize : '12px' , color : pwMsg.ok ? '#2ECC71' : '#E74C3C' } } > { pwMsg . text } < / span >
) }
< / div >
< / div >
< / div >
{ /* Active sessions */ }
< div style = { cardBase } >
< h3 style = { sectionTitle } > Active Sessions < / h3 >
{ loading ? (
< p style = { { fontSize : '13px' , color : '#7A7D85' } } > Loading … < / p >
) : sessions . length === 0 ? (
< p style = { { fontSize : '13px' , color : '#7A7D85' } } > No active sessions . < / p >
) : (
< div className = "flex flex-col gap-2" >
{ sessions . map ( ( s ) = > (
< div
key = { s . id }
className = "flex items-center gap-3"
style = { {
padding : '12px 14px' ,
borderRadius : '8px' ,
border : '1px solid rgba(200,164,52,0.08)' ,
backgroundColor : 'rgba(255,255,255,0.02)' ,
} }
>
< Monitor size = { 18 } color = { s . current ? '#C8A434' : '#7A7D85' } style = { { flexShrink : 0 } } / >
< div className = "flex-1 min-w-0" >
< div style = { { fontSize : '13px' , color : '#E8E6E0' , fontWeight : 500 } } >
{ describeUserAgent ( s . userAgent ) }
{ s . current && (
< span style = { { fontSize : '10px' , color : '#C8A434' , marginLeft : '8px' , fontWeight : 600 } } > THIS DEVICE < / span >
) }
< / div >
< div style = { { fontSize : '11px' , color : '#7A7D85' } } >
{ s . ip ? ? 'unknown IP' } · last active { relativeTime ( s . lastSeenAt ) }
< / div >
< / div >
{ ! s . current && (
< button
onClick = { ( ) = > handleRevoke ( s . id ) }
className = "flex items-center gap-1.5 cursor-pointer transition-colors whitespace-nowrap"
style = { {
fontSize : '11px' ,
fontWeight : 600 ,
color : '#E74C3C' ,
background : 'transparent' ,
border : '1px solid rgba(231,76,60,0.4)' ,
borderRadius : '7px' ,
padding : '6px 12px' ,
} }
>
< LogOut size = { 13 } / > Sign out
< / button >
) }
< / div >
) ) }
< / div >
) }
< / div >
{ /* Login activity */ }
< div style = { cardBase } >
< h3 style = { sectionTitle } > Recent Login Activity < / h3 >
{ loading ? (
< p style = { { fontSize : '13px' , color : '#7A7D85' } } > Loading … < / p >
) : events . length === 0 ? (
< p style = { { fontSize : '13px' , color : '#7A7D85' } } > No login activity recorded yet . < / p >
) : (
< div className = "flex flex-col" >
{ events . map ( ( e , i ) = > (
< div
key = { e . id }
className = "flex items-center gap-3"
style = { {
padding : '10px 0' ,
borderTop : i === 0 ? 'none' : '1px solid rgba(200,164,52,0.06)' ,
} }
>
< span
style = { {
width : '8px' ,
height : '8px' ,
borderRadius : '50%' ,
backgroundColor : e.success ? '#2ECC71' : '#E74C3C' ,
flexShrink : 0 ,
} }
/ >
< div className = "flex-1 min-w-0" >
< span style = { { fontSize : '13px' , color : '#E8E6E0' } } >
{ e . success ? 'Successful login' : 'Failed login' }
< / span >
< span style = { { fontSize : '11px' , color : '#7A7D85' , marginLeft : '8px' } } >
{ e . username ? ? 'unknown' } · { e . ip ? ? 'unknown IP' }
< / span >
< / div >
< span style = { { fontSize : '11px' , color : '#7A7D85' , flexShrink : 0 } } > { relativeTime ( e . createdAt ) } < / span >
< / div >
) ) }
< / div >
) }
< p style = { { fontSize : '11px' , color : '#7A7D85' , marginTop : '14px' } } >
SSO ( Authentik ) and multi - user accounts are planned — see the project roadmap .
< / p >
< / div >
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
< / div >
)
}
2026-06-19 11:12:33 +00:00
const sectionComponents : Record < string , ( ) = > React . ReactElement > = {
2026-06-18 18:44:26 +00:00
profile : ProfileSection ,
appearance : AppearanceSection ,
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
security : SecuritySection ,
2026-06-18 18:44:26 +00:00
integrations : IntegrationsSection ,
notifications : NotificationsSection ,
data : DataBackupSection ,
about : AboutSection ,
}
export default function Settings() {
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
const [ searchParams , setSearchParams ] = useSearchParams ( )
const requestedTab = searchParams . get ( 'tab' )
const active = requestedTab && sectionComponents [ requestedTab ] ? requestedTab : 'profile'
2026-06-18 18:44:26 +00:00
const ActiveSection = sectionComponents [ active ]
Wire up Profile/Appearance/Security in user menu (#21)
* Add editable display-name field to generic integrations
Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop,
Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate
from the host/IP field, mirroring the SSH host rename pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Surface the new-integration name field as a labeled input
The name field for new generic integrations was a faint header input
with only placeholder text, easy to miss. Move it into the form grid
as a proper labeled "Name" field next to the other connection fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4
* Add file upload for SSH private key and certificate fields
Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting
its contents into the Private Key / OPKSSH Certificate fields.
* Fix SSH private key paste corrupting multi-line PEM format
Private Key and Certificate fields were single-line <input> elements,
which strip newlines on paste and corrupt PEM-formatted keys (causing
'Unsupported key format' errors). Render them as multi-line textareas
instead so pasted keys keep their line breaks.
* Add JSON-converted bookmark import file for Archnest data import
Converts homarr-bookmarks.md into the format expected by /api/data/import.
* Auto-populate bookmark icons via favicon service in import JSON
Each bookmark now points to Google's favicon endpoint for its domain
instead of having no icon at all.
* Wire up Profile/Appearance/Security in the user menu
Profile, Appearance, and Security were dead "#" links. Added URL-based
tab deep-linking to the Settings page (?tab=profile|appearance|security|...)
and pointed the menu items at it. Added a Security tab placeholder ahead
of password/sessions/login-log/SSO work.
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-20 09:44:43 -04:00
function setActive ( id : string ) {
setSearchParams ( { tab : id } )
}
2026-06-18 18:44:26 +00:00
return (
< div className = "flex h-full w-full gap-5" >
{ /* Settings nav */ }
< div className = "flex flex-col gap-1 shrink-0" style = { { width : '200px' } } >
{ navSections . map ( ( s ) = > {
const Icon = s . icon
const isActive = active === s . id
return (
< button
key = { s . id }
onClick = { ( ) = > setActive ( s . id ) }
className = "flex items-center gap-2.5 cursor-pointer border-none bg-transparent transition-colors"
style = { {
fontSize : '13px' ,
fontWeight : 500 ,
padding : '10px 14px' ,
borderRadius : '8px' ,
color : isActive ? '#C8A434' : '#7A7D85' ,
backgroundColor : isActive ? 'rgba(200,164,52,0.1)' : 'transparent' ,
} }
>
< Icon size = { 15 } / >
{ s . label }
< / button >
)
} ) }
< / div >
{ /* Content */ }
< div className = "flex-1 overflow-y-auto" style = { { scrollbarWidth : 'none' } } >
< ActiveSection / >
< / div >
< / div >
)
}