2026-03-29 00:26:41 +01:00
const { PluginSettingTab , Setting , Notice } = require ( "obsidian" ) ;
const api = require ( "./api" ) ;
const auth = require ( "./auth" ) ;
2026-03-30 21:05:47 +02:00
const { isCoreSyncEnabled } = require ( "./core-sync-guard" ) ;
const { renderLogViewer } = require ( "./log-viewer" ) ;
2026-03-29 00:26:41 +01:00
class HeadlessSyncSettingTab extends PluginSettingTab {
constructor ( app , plugin ) {
super ( app , plugin ) ;
this . _cancelWait = null ;
2026-03-30 21:05:47 +02:00
this . _logCleanup = null ;
// Persistent container refs
this . _authEl = null ;
this . _syncEl = null ;
this . _logsEl = null ;
this . _logsRendered = false ;
2026-03-29 00:26:41 +01:00
}
async display ( ) {
2026-03-30 21:05:47 +02:00
// Clean up previous log listener before rebuilding
if ( this . _logCleanup ) {
this . _logCleanup ( ) ;
this . _logCleanup = null ;
}
2026-03-29 00:26:41 +01:00
const { containerEl } = this ;
containerEl . empty ( ) ;
2026-03-30 21:05:47 +02:00
this . _logsRendered = false ;
if ( isCoreSyncEnabled ( ) ) {
const syncWarningSetting = new Setting ( containerEl )
. setName ( "Obsidian Sync is active" ) ;
syncWarningSetting . descEl . createEl ( "span" , {
text : "Headless Sync cannot run alongside Obsidian's built-in sync to avoid conflicts. Disable Obsidian Sync in Core Plugins to use Headless Sync instead." ,
cls : "mod-warning" ,
} ) ;
syncWarningSetting
. addButton ( ( btn ) => {
btn . setButtonText ( "Open Core Plugins" ) . onClick ( ( ) => {
this . app . setting . openTabById ( "plugins" ) ;
} ) ;
} ) ;
return ;
}
2026-03-29 00:26:41 +01:00
let serverStatus ;
try {
serverStatus = await api . getStatus ( ) ;
} catch ( e ) {
containerEl . createEl ( "p" , {
text : "Failed to connect to Headless Sync server plugin." ,
cls : "mod-warning" ,
} ) ;
return ;
}
if ( ! serverStatus . installed ) {
containerEl . createEl ( "p" , {
text : "obsidian-headless (ob CLI) is not installed on the server. Install it to enable sync." ,
cls : "mod-warning" ,
} ) ;
return ;
}
2026-03-30 21:05:47 +02:00
this . _authEl = containerEl . createDiv ( ) ;
this . _syncEl = containerEl . createDiv ( ) ;
this . _logsEl = containerEl . createDiv ( ) ;
this . renderAuthSection ( serverStatus ) ;
await this . renderSyncSection ( serverStatus . authenticated ) ;
2026-03-29 00:26:41 +01:00
}
2026-03-30 21:05:47 +02:00
renderAuthSection ( serverStatus ) {
this . _authEl . empty ( ) ;
2026-03-29 00:26:41 +01:00
const localToken = auth . getObsidianSyncToken ( ) ;
if ( serverStatus . authenticated ) {
2026-03-30 21:05:47 +02:00
new Setting ( this . _authEl )
2026-03-29 00:26:41 +01:00
. setName ( "Obsidian Sync account" )
. setDesc (
` Signed in as ${ serverStatus . name || "unknown" } ( ${ serverStatus . email || "unknown" } ) ` ,
)
. addButton ( ( btn ) => {
2026-03-29 13:22:46 +02:00
btn . setButtonText ( "Disconnect" ) ;
btn . buttonEl . addClass ( "mod-destructive" ) ;
btn . onClick ( async ( ) => {
try {
await api . logout ( ) ;
new Notice ( "Disconnected from Headless Sync" ) ;
2026-03-30 21:05:47 +02:00
const status = await api . getStatus ( ) ;
this . renderAuthSection ( status ) ;
await this . renderSyncSection ( status . authenticated ) ;
2026-03-29 13:22:46 +02:00
} catch ( e ) {
new Notice ( ` Failed to disconnect: ${ e . message } ` ) ;
}
} ) ;
2026-03-29 00:26:41 +01:00
} ) ;
} else if ( localToken ) {
2026-03-30 21:05:47 +02:00
new Setting ( this . _authEl )
2026-03-29 00:26:41 +01:00
. setName ( "Obsidian Sync account detected" )
. setDesc ( ` ${ localToken . name } ( ${ localToken . email } ) ` )
. addButton ( ( btn ) => {
btn
. setButtonText ( "Use this account for Headless Sync" )
. setCta ( )
. onClick ( async ( ) => {
try {
await auth . sendTokenToServer ( localToken ) ;
new Notice ( "Connected to Headless Sync" ) ;
2026-03-30 21:05:47 +02:00
const status = await api . getStatus ( ) ;
this . renderAuthSection ( status ) ;
await this . renderSyncSection ( status . authenticated ) ;
2026-03-29 00:26:41 +01:00
} catch ( e ) {
new Notice ( ` Failed to connect: ${ e . message } ` ) ;
}
} ) ;
} ) ;
} else {
2026-03-30 21:05:47 +02:00
new Setting ( this . _authEl )
2026-03-29 00:26:41 +01:00
. setName ( "Obsidian Sync account" )
. setDesc ( "Sign in to your Obsidian account to enable sync." )
. addButton ( ( btn ) => {
btn . setButtonText ( "Log in to Obsidian Sync" ) . onClick ( ( ) => {
2026-03-30 21:05:47 +02:00
const triggered = auth . triggerLogin ( this . app ) ;
2026-03-29 00:26:41 +01:00
2026-03-30 21:05:47 +02:00
if ( ! triggered ) {
new Notice (
"Could not open login dialog. Try logging in from Settings > General." ,
) ;
return ;
}
2026-03-29 00:26:41 +01:00
2026-03-30 21:05:47 +02:00
this . _cancelWait = auth . waitForLogin ( async ( token ) => {
this . _cancelWait = null ;
2026-03-29 00:26:41 +01:00
2026-03-30 21:05:47 +02:00
if ( token ) {
new Notice ( ` Detected login: ${ token . name } ` ) ;
const status = await api . getStatus ( ) ;
this . renderAuthSection ( status ) ;
await this . renderSyncSection ( status . authenticated ) ;
}
2026-03-29 00:26:41 +01:00
} ) ;
2026-03-30 21:05:47 +02:00
} ) ;
2026-03-29 00:26:41 +01:00
} ) ;
}
}
2026-03-30 21:05:47 +02:00
async renderSyncSection ( authenticated ) {
this . _syncEl . empty ( ) ;
this . _syncEl . createEl ( "h3" , { text : "Vault sync" } ) ;
2026-03-29 13:22:46 +02:00
if ( ! authenticated ) {
2026-03-30 21:05:47 +02:00
new Setting ( this . _syncEl )
2026-03-29 13:22:46 +02:00
. setName ( "Sync not configured" )
. setDesc ( "Sign in to your Obsidian Sync account to set up sync." )
. addButton ( ( btn ) => {
btn . setButtonText ( "Set up sync" ) ;
btn . buttonEl . disabled = true ;
} ) ;
return ;
}
2026-03-29 00:26:41 +01:00
const vaultId = this . app . vault . getName ( ) ;
let vaultsData ;
try {
vaultsData = await api . getVaults ( ) ;
} catch ( e ) {
2026-03-30 21:05:47 +02:00
this . _syncEl . createEl ( "p" , {
2026-03-29 00:26:41 +01:00
text : ` Failed to load sync state: ${ e . message } ` ,
cls : "mod-warning" ,
} ) ;
return ;
}
const vaultState = vaultsData . vaults . find ( ( v ) => v . vaultId === vaultId ) ;
if ( ! vaultState ) {
2026-03-30 21:05:47 +02:00
new Setting ( this . _syncEl )
2026-03-29 00:26:41 +01:00
. setName ( "Sync not configured" )
. setDesc ( "This vault has not been linked to a remote vault yet." )
. addButton ( ( btn ) => {
btn
. setButtonText ( "Set up sync" )
. setCta ( )
. onClick ( ( ) => {
2026-03-29 13:22:46 +02:00
const scope = this . app . setting . scope ;
const prevFocusContainer = scope . tabFocusContainerEl ;
scope . tabFocusContainerEl = null ;
const cleanup = ( ) => {
scope . tabFocusContainerEl = prevFocusContainer ;
} ;
const modal = new window . IgnisUI . SyncSetupModal ( {
target : document . body ,
props : {
vaultId ,
2026-03-30 21:05:47 +02:00
onSuccess : async ( ) => {
2026-03-29 13:22:46 +02:00
cleanup ( ) ;
modal . $destroy ( ) ;
2026-03-30 21:05:47 +02:00
await this . renderSyncSection ( true ) ;
2026-03-29 13:22:46 +02:00
} ,
} ,
} ) ;
modal . $on ( "close" , ( ) => {
cleanup ( ) ;
modal . $destroy ( ) ;
} ) ;
2026-03-29 00:26:41 +01:00
} ) ;
} ) ;
return ;
}
// Show current sync config
2026-03-30 21:05:47 +02:00
new Setting ( this . _syncEl )
2026-03-29 00:26:41 +01:00
. setName ( "Remote vault" )
2026-03-30 21:05:47 +02:00
. setDesc (
vaultState . remoteVaultName || vaultState . remoteVault || "unknown" ,
)
2026-03-29 13:22:46 +02:00
. addButton ( ( btn ) => {
btn . setButtonText ( "Unlink" ) ;
btn . buttonEl . addClass ( "mod-destructive" ) ;
btn . onClick ( async ( ) => {
try {
await api . unlinkVault ( vaultId ) ;
new Notice ( "Vault unlinked" ) ;
2026-03-30 21:05:47 +02:00
await this . renderSyncSection ( true ) ;
2026-03-29 13:22:46 +02:00
} catch ( e ) {
new Notice ( ` Failed to unlink: ${ e . message } ` ) ;
}
} ) ;
} ) ;
2026-03-29 00:26:41 +01:00
2026-03-30 21:05:47 +02:00
new Setting ( this . _syncEl )
2026-03-29 00:26:41 +01:00
. setName ( "Sync mode" )
. setDesc ( vaultState . config ? . mode || "bidirectional" ) ;
// Sync controls
2026-03-30 21:05:47 +02:00
const controlsEl = this . _syncEl . createDiv ( ) ;
this . renderSyncControls ( controlsEl , vaultId , vaultState ) ;
// Log viewer - only render once, persists across sync section rebuilds
if ( ! this . _logsRendered ) {
await this . renderLogs ( this . _logsEl , vaultId ) ;
this . _logsRendered = true ;
}
}
async renderSyncControls ( containerEl , vaultId , vaultState ) {
containerEl . empty ( ) ;
if ( ! vaultState ) {
try {
const data = await api . getVaults ( ) ;
vaultState = ( data . vaults || [ ] ) . find ( ( v ) => v . vaultId === vaultId ) ;
} catch {
return ;
}
}
if ( ! vaultState ) {
return ;
}
2026-03-29 00:26:41 +01:00
const statusText =
vaultState . status === "running"
? "Sync is running"
: vaultState . status === "error"
? ` Error: ${ vaultState . error } `
: "Sync is stopped" ;
new Setting ( containerEl )
. setName ( "Status" )
. setDesc ( statusText )
. addButton ( ( btn ) => {
if ( vaultState . status === "running" ) {
2026-03-29 13:22:46 +02:00
btn . setButtonText ( "Stop sync" ) ;
btn . buttonEl . addClass ( "mod-destructive" ) ;
btn . onClick ( async ( ) => {
2026-03-29 00:26:41 +01:00
try {
await api . stopSync ( vaultId ) ;
new Notice ( "Sync stopped" ) ;
2026-03-30 21:05:47 +02:00
this . renderSyncControls ( containerEl , vaultId ) ;
2026-03-29 00:26:41 +01:00
} catch ( e ) {
new Notice ( ` Failed to stop: ${ e . message } ` ) ;
}
} ) ;
} else {
2026-03-30 21:05:47 +02:00
btn
. setButtonText ( "Start sync" )
. setCta ( )
. onClick ( async ( ) => {
try {
await api . startSync ( vaultId ) ;
new Notice ( "Sync started" ) ;
this . renderSyncControls ( containerEl , vaultId ) ;
} catch ( e ) {
new Notice ( ` Failed to start: ${ e . message } ` ) ;
}
} ) ;
2026-03-29 00:26:41 +01:00
}
} ) ;
}
async renderLogs ( containerEl , vaultId ) {
2026-05-24 21:51:02 +02:00
this . _logCleanup = await renderLogViewer ( containerEl , vaultId ) ;
2026-03-29 00:26:41 +01:00
}
hide ( ) {
if ( this . _cancelWait ) {
this . _cancelWait ( ) ;
this . _cancelWait = null ;
}
2026-03-30 15:33:53 +02:00
if ( this . _logCleanup ) {
this . _logCleanup ( ) ;
this . _logCleanup = null ;
}
2026-03-29 00:26:41 +01:00
super . hide ( ) ;
}
}
module . exports = { HeadlessSyncSettingTab } ;