@@ -337,6 +337,7 @@
currentSchedulerStatus = st || null ;
renderStatus ( st ) ;
renderSchedulerInterleaveStatus ( ) ;
renderActivityLog ( ) ;
renderSatPassStatus ( ) ;
} )
. catch ( function ( ) { } ) ;
@@ -364,7 +365,57 @@
const satLabel = st . active _satellite
? " [SAT: " + st . active _satellite + "]"
: "" ;
el . textContent = "Last applied: " + name + satLabel + ts ;
var details = "" ;
if ( st . freq _hz ) {
details += formatFreq ( st . freq _hz ) ;
if ( st . mode ) details += " \u00B7 " + st . mode ;
if ( st . active _decoders && st . active _decoders . length > 0 ) {
details += " \u00B7 " + st . active _decoders . join ( ", " ) + " active" ;
}
}
if ( details ) {
el . innerHTML = "Last applied: " + escHtml ( name ) + satLabel + ts +
'<br><span class="sch-status-detail">' + escHtml ( details ) + '</span>' ;
} else {
el . textContent = "Last applied: " + name + satLabel + ts ;
}
}
// -------------------------------------------------------------------------
// Activity log
// -------------------------------------------------------------------------
function apiGetSchedulerLog ( rigId ) {
return fetch ( "/scheduler/" + encodeURIComponent ( rigId ) + "/log" ) . then ( function ( r ) {
return r . ok ? r . json ( ) : [ ] ;
} ) ;
}
function renderActivityLog ( ) {
var wrap = document . getElementById ( "scheduler-activity-log-wrap" ) ;
var container = document . getElementById ( "scheduler-activity-log" ) ;
if ( ! wrap || ! container || ! currentRigId ) return ;
apiGetSchedulerLog ( currentRigId ) . then ( function ( entries ) {
if ( ! entries || entries . length === 0 ) {
wrap . style . display = "none" ;
return ;
}
wrap . style . display = "" ;
var html = entries . slice ( ) . reverse ( ) . map ( function ( e ) {
var d = new Date ( e . utc * 1000 ) ;
var ts = d . toUTCString ( ) ;
var action = e . action || "unknown" ;
var label = e . entry _label || "" ;
var bm = e . bookmark _name || "" ;
return '<div class="sch-log-entry">' +
'<span class="sch-log-time">' + escHtml ( ts ) + '</span> ' +
'<span class="sch-log-action">' + escHtml ( action ) + '</span> ' +
( bm ? '<span class="sch-log-bm">' + escHtml ( bm ) + '</span>' : '' ) +
( label ? ' <span class="sch-log-label">(' + escHtml ( label ) + ')</span>' : '' ) +
'</div>' ;
} ) . join ( "" ) ;
container . innerHTML = html ;
} ) . catch ( function ( ) { } ) ;
}
// -------------------------------------------------------------------------
@@ -402,6 +453,10 @@
const lon = gl . lon != null ? gl . lon : ( typeof serverLon !== "undefined" ? serverLon : "" ) ;
setInputValue ( "scheduler-gl-lat" , lat != null ? lat : "" ) ;
setInputValue ( "scheduler-gl-lon" , lon != null ? lon : "" ) ;
var gridEl = document . getElementById ( "scheduler-gl-grid" ) ;
if ( gridEl && lat !== "" && lon !== "" ) {
gridEl . value = latLonToGrid ( lat , lon ) ;
}
setInputValue ( "scheduler-gl-window" , gl . transition _window _min != null ? gl . transition _window _min : 20 ) ;
renderBookmarkSelect ( "scheduler-gl-dawn" , gl . dawn _bookmark _id ) ;
renderBookmarkSelect ( "scheduler-gl-day" , gl . day _bookmark _id ) ;
@@ -413,6 +468,10 @@
const lon = typeof serverLon !== "undefined" ? serverLon : "" ;
setInputValue ( "scheduler-gl-lat" , lat != null ? lat : "" ) ;
setInputValue ( "scheduler-gl-lon" , lon != null ? lon : "" ) ;
var gridEl2 = document . getElementById ( "scheduler-gl-grid" ) ;
if ( gridEl2 && lat !== "" && lon !== "" ) {
gridEl2 . value = latLonToGrid ( lat , lon ) ;
}
setInputValue ( "scheduler-gl-window" , 20 ) ;
renderBookmarkSelect ( "scheduler-gl-dawn" , null ) ;
renderBookmarkSelect ( "scheduler-gl-day" , null ) ;
@@ -596,7 +655,7 @@
}
var W = 1000 ;
var H = 62 ;
var H = 80 ;
var BAR _Y = 6 ;
var BAR _H = 30 ;
var TICK _Y = BAR _Y + BAR _H + 2 ;
@@ -634,6 +693,31 @@
}
} ) ;
// Interleave stripes for overlapping entries
var interleaveMin = currentConfig && currentConfig . interleave _min ? Number ( currentConfig . interleave _min ) : 0 ;
if ( interleaveMin > 0 && entries . length > 1 ) {
// Find overlap regions where 2+ entries are active
for ( var m = 0 ; m < 1440 ; m += interleaveMin ) {
var overlapping = [ ] ;
entries . forEach ( function ( entry , idx ) {
if ( schedulerEntryIsActive ( entry , m ) ) {
overlapping . push ( idx ) ;
}
} ) ;
if ( overlapping . length > 1 ) {
var stripeX = ( m / 1440 ) * W ;
var stripeW = Math . max ( 1 , ( interleaveMin / 1440 ) * W ) ;
// Determine which entry "owns" this stripe via cycle position
var cyclePos = m % ( interleaveMin * overlapping . length ) ;
var ownerSlot = Math . floor ( cyclePos / interleaveMin ) ;
var ownerIdx = overlapping [ ownerSlot % overlapping . length ] ;
var stripeColor = TIMELINE _COLORS [ ownerIdx % TIMELINE _COLORS . length ] ;
svg += '<rect x="' + stripeX . toFixed ( 1 ) + '" y="' + ( BAR _Y + BAR _H - 5 ) + '" width="' + stripeW . toFixed ( 1 ) +
'" height="5" fill="' + stripeColor + '" opacity="0.9" />' ;
}
}
}
// Tick marks every 3 hours
for ( var h = 0 ; h <= 24 ; h += 3 ) {
var tx = ( h / 24 ) * W ;
@@ -645,6 +729,17 @@
}
}
// Local time ticks
var LOCAL _TICK _Y = TICK _Y + 18 ;
for ( var h = 0 ; h < 24 ; h += 3 ) {
var localMin = h * 60 ;
var utcOffset = new Date ( ) . getTimezoneOffset ( ) ; // offset in minutes (negative for east of UTC)
var utcMin = ( localMin + utcOffset + 1440 ) % 1440 ;
var tx = ( utcMin / 1440 ) * W ;
svg += '<text class="sch-timeline-tick-label sch-timeline-local-tick" x="' + ( tx + 3 ) . toFixed ( 1 ) + '" y="' + ( LOCAL _TICK _Y + 10 ) +
'">' + String ( h ) . padStart ( 2 , "0" ) + 'L</text>' ;
}
// Current time needle
svg += '<g id="sch-timeline-needle-g">' + timelineNeedleSvg ( ) + '</g>' ;
@@ -659,6 +754,29 @@
if ( entry ) schOpenEntryForm ( entry , i ) ;
} ) ;
} ) ;
// Click-to-add on empty timeline region
var svgEl = container . querySelector ( 'svg' ) ;
if ( svgEl ) {
svgEl . addEventListener ( 'click' , function ( e ) {
// Only trigger if clicking on the background bar, not on a segment
if ( e . target . classList . contains ( 'sch-timeline-seg' ) ) return ;
var rect = svgEl . getBoundingClientRect ( ) ;
var xPct = ( e . clientX - rect . left ) / rect . width ;
var clickMin = Math . floor ( xPct * 1440 ) ;
var startHour = Math . floor ( clickMin / 60 ) ;
var startMin = startHour * 60 ;
var endMin = ( ( startHour + 1 ) % 24 ) * 60 ;
// Pre-fill the entry form with the clicked hour
schOpenEntryForm ( null , null ) ;
var startEl = document . getElementById ( 'scheduler-ts-start' ) ;
var endEl = document . getElementById ( 'scheduler-ts-end' ) ;
if ( startEl ) startEl . value = minToHHMM ( startMin ) ;
if ( endEl ) endEl . value = minToHHMM ( endMin ) ;
} ) ;
svgEl . style . cursor = 'crosshair' ;
}
}
function timelineNeedleSvg ( ) {
@@ -675,6 +793,57 @@
if ( g ) g . innerHTML = timelineNeedleSvg ( ) ;
}
// -------------------------------------------------------------------------
// Inline row editing
// -------------------------------------------------------------------------
function schInlineEdit ( tr , entry , idx ) {
var bmOptions = bookmarkList . map ( function ( bm ) {
var sel = bm . id === entry . bookmark _id ? ' selected' : '' ;
return '<option value="' + escHtml ( bm . id ) + '"' + sel + '>' + escHtml ( bm . name ) + '</option>' ;
} ) . join ( '' ) ;
tr . innerHTML =
'<td class="sch-drag-handle" draggable="true" title="Drag to reorder">\u2807</td>' +
'<td><input type="time" class="status-input sch-inline-input" value="' + minToHHMM ( entry . start _min ) + '" data-field="start" /></td>' +
'<td><input type="time" class="status-input sch-inline-input" value="' + minToHHMM ( entry . end _min ) + '" data-field="end" /></td>' +
'<td>' + ( entry . center _hz ? formatFreq ( entry . center _hz ) : '\u2014' ) + '</td>' +
'<td><select class="status-input sch-inline-input" data-field="bookmark">' + bmOptions + '</select></td>' +
'<td>' + ( Array . isArray ( entry . bookmark _ids ) && entry . bookmark _ids . length ? entry . bookmark _ids . map ( function ( id ) { return escHtml ( bmName ( id ) ) ; } ) . join ( ', ' ) : '\u2014' ) + '</td>' +
'<td><input type="text" class="status-input sch-inline-input" value="' + escHtml ( entry . label || '' ) + '" data-field="label" /></td>' +
'<td><input type="number" class="status-input sch-inline-input" value="' + ( entry . interleave _min || '' ) + '" min="1" max="60" placeholder="\u2014" data-field="interleave" style="width:4rem;" /></td>' +
'<td><input type="checkbox" ' + ( entry . record ? 'checked' : '' ) + ' data-field="record" /></td>' +
'<td><button class="sch-write sch-inline-save" type="button">Save</button><button class="sch-write sch-inline-cancel" type="button">Cancel</button></td>' ;
tr . classList . add ( 'sch-inline-editing' ) ;
tr . querySelector ( '.sch-inline-save' ) . addEventListener ( 'click' , function ( ) {
var startEl = tr . querySelector ( '[data-field="start"]' ) ;
var endEl = tr . querySelector ( '[data-field="end"]' ) ;
var bmEl = tr . querySelector ( '[data-field="bookmark"]' ) ;
var labelEl = tr . querySelector ( '[data-field="label"]' ) ;
var ilEl = tr . querySelector ( '[data-field="interleave"]' ) ;
var recEl = tr . querySelector ( '[data-field="record"]' ) ;
if ( bmEl && ! bmEl . value ) { alert ( 'Please select a bookmark.' ) ; return ; }
entry . start _min = hhmmToMin ( startEl . value ) ;
entry . end _min = hhmmToMin ( endEl . value ) ;
entry . bookmark _id = bmEl . value ;
entry . label = labelEl . value . trim ( ) || null ;
var ilVal = parseInt ( ilEl . value , 10 ) ;
entry . interleave _min = ( ! isNaN ( ilVal ) && ilVal > 0 ) ? ilVal : null ;
entry . record = recEl . checked ;
currentConfig . entries [ idx ] = entry ;
renderTimespanEntries ( ) ;
markSchedulerDirty ( ) ;
} ) ;
tr . querySelector ( '.sch-inline-cancel' ) . addEventListener ( 'click' , function ( ) {
renderTimespanEntries ( ) ;
} ) ;
}
// -------------------------------------------------------------------------
// TimeSpan entries table
// -------------------------------------------------------------------------
@@ -688,6 +857,10 @@
: [ ] ;
entries . forEach ( function ( entry , idx ) {
const tr = document . createElement ( "tr" ) ;
if ( currentSchedulerStatus && currentSchedulerStatus . last _entry _id &&
entry . id && String ( entry . id ) === String ( currentSchedulerStatus . last _entry _id ) ) {
tr . classList . add ( "sch-active" ) ;
}
const il = entry . interleave _min ? String ( entry . interleave _min ) + " min" : "—" ;
const allDay = entry . start _min === entry . end _min ;
const centerCell = entry . center _hz ? formatFreq ( entry . center _hz ) : "—" ;
@@ -696,8 +869,9 @@
? extraIds . map ( function ( id ) { return escHtml ( bmName ( id ) ) ; } ) . join ( ", " )
: "—" ;
tr . innerHTML =
'<td>' + ( allDay ? "All day" : minToHHMM ( entry . start _min ) ) + ' </td>' +
'<td>' + ( allDay ? "— " : minToHHMM ( entry . end _min ) ) + '</td>' +
'<td class="sch-drag-handle" draggable="true" title="Drag to reorder">\u2807 </td>' +
'<td>' + ( allDay ? "All day " : minToHHMM ( entry . start _min ) + ' <span class="sch-local-time">(' + minToLocal ( entry . start _min ) + ')</span>' ) + '</td>' +
'<td>' + ( allDay ? "\u2014" : minToHHMM ( entry . end _min ) + ' <span class="sch-local-time">(' + minToLocal ( entry . end _min ) + ')</span>' ) + '</td>' +
'<td>' + centerCell + '</td>' +
'<td>' + escHtml ( bmName ( entry . bookmark _id ) ) + '</td>' +
'<td>' + extraCell + '</td>' +
@@ -714,7 +888,7 @@
btn . addEventListener ( "click" , function ( ) {
const i = parseInt ( btn . dataset . idx , 10 ) ;
const entry = currentConfig && currentConfig . entries ? currentConfig . entries [ i ] : null ;
if ( entry ) schOpenEntryForm ( entry , i ) ;
if ( entry ) schInlineEdit ( btn . closest ( 'tr' ) , entry , i ) ;
} ) ;
} ) ;
tbody . querySelectorAll ( ".sch-remove-btn" ) . forEach ( function ( btn ) {
@@ -722,6 +896,50 @@
removeEntry ( parseInt ( btn . dataset . idx , 10 ) ) ;
} ) ;
} ) ;
// Drag-to-reorder
( function ( ) {
var handles = tbody . querySelectorAll ( '.sch-drag-handle' ) ;
var dragIdx = null ;
handles . forEach ( function ( handle , idx ) {
var row = handle . parentElement ;
handle . addEventListener ( 'dragstart' , function ( e ) {
dragIdx = idx ;
row . classList . add ( 'sch-dragging' ) ;
e . dataTransfer . effectAllowed = 'move' ;
e . dataTransfer . setData ( 'text/plain' , String ( idx ) ) ;
} ) ;
row . addEventListener ( 'dragover' , function ( e ) {
e . preventDefault ( ) ;
e . dataTransfer . dropEffect = 'move' ;
row . classList . add ( 'sch-drag-over' ) ;
} ) ;
row . addEventListener ( 'dragleave' , function ( ) {
row . classList . remove ( 'sch-drag-over' ) ;
} ) ;
row . addEventListener ( 'drop' , function ( e ) {
e . preventDefault ( ) ;
row . classList . remove ( 'sch-drag-over' ) ;
if ( dragIdx === null || dragIdx === idx ) return ;
var entries = currentConfig . entries ;
var moved = entries . splice ( dragIdx , 1 ) [ 0 ] ;
entries . splice ( idx , 0 , moved ) ;
renderTimespanEntries ( ) ;
markSchedulerDirty ( ) ;
} ) ;
handle . addEventListener ( 'dragend' , function ( ) {
row . classList . remove ( 'sch-dragging' ) ;
dragIdx = null ;
} ) ;
} ) ;
} ) ( ) ;
renderTimeline ( ) ;
}
@@ -730,6 +948,15 @@
return bm ? bm . name : String ( id || "" ) ;
}
function minToLocal ( min ) {
// Convert UTC minutes-since-midnight to local time string
var now = new Date ( ) ;
var utcMidnight = new Date ( Date . UTC ( now . getUTCFullYear ( ) , now . getUTCMonth ( ) , now . getUTCDate ( ) ) ) ;
var utcMs = utcMidnight . getTime ( ) + min * 60000 ;
var local = new Date ( utcMs ) ;
return String ( local . getHours ( ) ) . padStart ( 2 , "0" ) + ":" + String ( local . getMinutes ( ) ) . padStart ( 2 , "0" ) ;
}
function minToHHMM ( min ) {
const h = Math . floor ( min / 60 ) % 24 ;
const m = min % 60 ;
@@ -741,6 +968,43 @@
return parseInt ( parts [ 0 ] || "0" , 10 ) * 60 + parseInt ( parts [ 1 ] || "0" , 10 ) ;
}
function gridToLatLon ( grid ) {
grid = String ( grid ) . toUpperCase ( ) . trim ( ) ;
if ( grid . length < 4 ) return null ;
var lonField = grid . charCodeAt ( 0 ) - 65 ;
var latField = grid . charCodeAt ( 1 ) - 65 ;
var lonSquare = parseInt ( grid . charAt ( 2 ) , 10 ) ;
var latSquare = parseInt ( grid . charAt ( 3 ) , 10 ) ;
if ( isNaN ( lonSquare ) || isNaN ( latSquare ) || lonField < 0 || lonField > 17 || latField < 0 || latField > 17 ) return null ;
var lon = lonField * 20 + lonSquare * 2 - 180 ;
var lat = latField * 10 + latSquare * 1 - 90 ;
if ( grid . length >= 6 ) {
var lonSub = grid . charCodeAt ( 4 ) - 65 ;
var latSub = grid . charCodeAt ( 5 ) - 65 ;
if ( lonSub >= 0 && lonSub < 24 && latSub >= 0 && latSub < 24 ) {
lon += lonSub * ( 2 / 24 ) + ( 1 / 24 ) ;
lat += latSub * ( 1 / 24 ) + ( 0.5 / 24 ) ;
}
} else {
lon += 1 ; // center of square
lat += 0.5 ;
}
return { lat : lat , lon : lon } ;
}
function latLonToGrid ( lat , lon ) {
lon = parseFloat ( lon ) + 180 ;
lat = parseFloat ( lat ) + 90 ;
if ( isNaN ( lon ) || isNaN ( lat ) ) return "" ;
var lonField = String . fromCharCode ( 65 + Math . floor ( lon / 20 ) ) ;
var latField = String . fromCharCode ( 65 + Math . floor ( lat / 10 ) ) ;
var lonSquare = Math . floor ( ( lon % 20 ) / 2 ) ;
var latSquare = Math . floor ( lat % 10 ) ;
var lonSub = String . fromCharCode ( 97 + Math . floor ( ( ( lon % 2 ) / 2 ) * 24 ) ) ;
var latSub = String . fromCharCode ( 97 + Math . floor ( ( lat % 1 ) * 24 ) ) ;
return lonField + latField + lonSquare + latSquare + lonSub + latSub ;
}
function escHtml ( s ) {
return String ( s )
. replace ( /&/g , "&" )
@@ -795,6 +1059,14 @@
markSchedulerDirty ( ) ;
}
// -------------------------------------------------------------------------
// Bookmark existence check
// -------------------------------------------------------------------------
function bookmarkExists ( id ) {
if ( ! id ) return true ; // null/empty is allowed
return bookmarkList . some ( function ( bm ) { return bm . id === id ; } ) ;
}
// -------------------------------------------------------------------------
// Save
// -------------------------------------------------------------------------
@@ -835,6 +1107,47 @@
// Satellite overlay — saved regardless of base mode.
config . satellites = collectSatelliteConfig ( ) ;
// Validate bookmark existence before saving
var missingBmErrors = [ ] ;
if ( mode === "grayline" && config . grayline ) {
var gl = config . grayline ;
var glFields = [
[ "dawn_bookmark_id" , "Grayline dawn" ] ,
[ "day_bookmark_id" , "Grayline day" ] ,
[ "dusk_bookmark_id" , "Grayline dusk" ] ,
[ "night_bookmark_id" , "Grayline night" ] ,
] ;
glFields . forEach ( function ( pair ) {
if ( ! bookmarkExists ( gl [ pair [ 0 ] ] ) ) missingBmErrors . push ( pair [ 1 ] + " (bookmark " + gl [ pair [ 0 ] ] + ")" ) ;
} ) ;
}
if ( mode === "time_span" && Array . isArray ( config . entries ) ) {
config . entries . forEach ( function ( entry , idx ) {
var label = entry . label || "Entry #" + ( idx + 1 ) ;
if ( ! bookmarkExists ( entry . bookmark _id ) ) {
missingBmErrors . push ( label + " primary bookmark (" + entry . bookmark _id + ")" ) ;
}
var extras = Array . isArray ( entry . bookmark _ids ) ? entry . bookmark _ids : [ ] ;
extras . forEach ( function ( id ) {
if ( ! bookmarkExists ( id ) ) {
missingBmErrors . push ( label + " extra channel (" + id + ")" ) ;
}
} ) ;
} ) ;
}
if ( config . satellites && Array . isArray ( config . satellites . entries ) ) {
config . satellites . entries . forEach ( function ( sat , idx ) {
var satLabel = sat . name || "Satellite #" + ( idx + 1 ) ;
if ( ! bookmarkExists ( sat . bookmark _id ) ) {
missingBmErrors . push ( satLabel + " bookmark (" + sat . bookmark _id + ")" ) ;
}
} ) ;
}
if ( missingBmErrors . length > 0 ) {
showSchedulerToast ( "Missing bookmarks: " + missingBmErrors . join ( "; " ) , true ) ;
return ;
}
const btn = document . getElementById ( "scheduler-save-btn" ) ;
if ( btn ) btn . disabled = true ;
@@ -963,6 +1276,33 @@
} ) ;
}
// Grid square ↔ lat/lon sync
var gridEl = document . getElementById ( "scheduler-gl-grid" ) ;
if ( gridEl ) {
gridEl . addEventListener ( "input" , function ( ) {
var ll = gridToLatLon ( gridEl . value ) ;
if ( ll ) {
setInputValue ( "scheduler-gl-lat" , ll . lat . toFixed ( 3 ) ) ;
setInputValue ( "scheduler-gl-lon" , ll . lon . toFixed ( 3 ) ) ;
markSchedulerDirty ( ) ;
}
} ) ;
}
var latEl = document . getElementById ( "scheduler-gl-lat" ) ;
var lonEl = document . getElementById ( "scheduler-gl-lon" ) ;
[ latEl , lonEl ] . forEach ( function ( el ) {
if ( el ) {
el . addEventListener ( "input" , function ( ) {
var la = parseFloat ( document . getElementById ( "scheduler-gl-lat" ) . value ) ;
var lo = parseFloat ( document . getElementById ( "scheduler-gl-lon" ) . value ) ;
var gEl = document . getElementById ( "scheduler-gl-grid" ) ;
if ( gEl && ! isNaN ( la ) && ! isNaN ( lo ) ) {
gEl . value = latLonToGrid ( la , lo ) ;
}
} ) ;
}
} ) ;
wireExtraBmAdd ( ) ;
wireSatelliteEvents ( ) ;
}
@@ -988,25 +1328,36 @@
let pendingExtraBmIds = [ ] ;
function renderExtraBmList ( ) {
const container = document . getElementById ( "scheduler-ts-extra-bm-list" ) ;
var container = document . getElementById ( "scheduler-ts-extra-bm-list" ) ;
if ( ! container ) return ;
container . innerHTML = "" ;
pendingExtraBmIds . forEach ( function ( id , idx ) {
const bm = bookmarkList . find ( function ( b ) { return b . id === id ; } ) ;
const tag = document . createElement ( "span" ) ;
tag . className = "sch-extra-bm-tag " ;
tag . textContent = bm ? bm . name : id ;
const rm = document . createElement ( "span" ) ;
rm . className = "sch-extra-bm-rm " ;
rm . textContent = "× " ;
rm . title = "Remove" ;
rm . addEventListener ( "click" , function ( ) {
var bm = bookmarkList . find ( function ( b ) { return b . id === id ; } ) ;
var chip = document . createElement ( "span" ) ;
chip . className = "sch-extra-bm-chip " ;
var rmBtn = document . createElement ( "span" ) ;
rmBtn . className = "sch-extra-bm-chip-rm" ;
rmBtn . textContent = "\u00D7 " ;
rmBtn . title = "Remove " ;
rmBtn . addEventListener ( "click" , function ( ) {
pendingExtraBmIds . splice ( idx , 1 ) ;
renderExtraBmList ( ) ;
} ) ;
tag . appendChild ( rm ) ;
container . appendChild ( tag ) ;
chip . appendChild ( rmBtn ) ;
var label = document . createTextNode ( " " + ( bm ? bm . name : id ) ) ;
chip . appendChild ( label ) ;
container . appendChild ( chip ) ;
} ) ;
// Disable already-added bookmarks in dropdown
var pick = document . getElementById ( "scheduler-ts-extra-bm-pick" ) ;
if ( pick ) {
Array . from ( pick . options ) . forEach ( function ( opt ) {
if ( opt . value ) {
opt . disabled = pendingExtraBmIds . includes ( opt . value ) ;
}
} ) ;
}
}
function wireExtraBmAdd ( ) {
@@ -1047,13 +1398,53 @@
getConfig : function ( ) { return currentConfig ; } ,
getStatus : function ( ) { return currentSchedulerStatus ; } ,
getBookmarks : function ( ) { return bookmarkList ; } ,
markDirty : function ( ) { markSchedulerDirty ( ) ; } ,
} ;
if ( window . satScheduler ) window . satScheduler . wireEvents ( ) ;
}
// -------------------------------------------------------------------------
// Keyboard shortcuts for scheduler control
// -------------------------------------------------------------------------
function isInputFocused ( ) {
var el = document . activeElement ;
if ( ! el ) return false ;
var tag = el . tagName ;
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el . isContentEditable ;
}
document . addEventListener ( "keydown" , function ( e ) {
if ( isInputFocused ( ) ) return ;
if ( e . shiftKey && e . key === "R" ) {
e . preventDefault ( ) ;
// Toggle release to scheduler
var releaseBtn = document . getElementById ( "scheduler-release-btn" ) ;
if ( releaseBtn && ! releaseBtn . disabled ) releaseBtn . click ( ) ;
} else if ( e . shiftKey && e . key === "N" ) {
e . preventDefault ( ) ;
schedulerSelectRelativeEntry ( 1 ) ;
} else if ( e . shiftKey && e . key === "P" ) {
e . preventDefault ( ) ;
schedulerSelectRelativeEntry ( - 1 ) ;
}
} ) ;
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
// Persist details open/closed state
( function ( ) {
var details = document . querySelector ( ".sch-ts-details" ) ;
if ( ! details ) return ;
var key = "sch-details-open" ;
var saved = localStorage . getItem ( key ) ;
if ( saved !== null ) details . open = saved === "1" ;
details . addEventListener ( "toggle" , function ( ) {
localStorage . setItem ( key , details . open ? "1" : "0" ) ;
} ) ;
} ) ( ) ;
window . initScheduler = initScheduler ;
window . destroyScheduler = destroyScheduler ;
window . wireSchedulerEvents = wireSchedulerEvents ;