@@ -633,230 +633,331 @@
// ---- Wiring ------------------------------------------------------------------
// Every computer and device the panel knows and what connects them, like a wiring diagram (2026-10-27). Drag a
// machine to arrange it; click one to see its scripts and connections; hide connection types or single
// connections. Positions and what is hidden are kept in this browser only.
const W _W = 2000 , W _H = 1300 ;
const wire = { data : null , pos : null , hidden : null , types : null , sel : null , focus : false , problems : false , drag : null , view : null } ;
const wireRead = ( k , d ) => { try { return JSON . parse ( store . get ( k ) || "null" ) || d ; } catch ( e ) { return d ; } } ;
const wireSave = ( ) => { store . set ( "sp-wire-pos" , JSON . stringify ( wire . pos ) ) ; store . set ( "sp-wire-hidden" , JSON . stringify ( [ ... wire . hidden ] ) ) ; store . set ( "sp-wire-types" , JSON . stringify ( [ ... wire . types ] ) ) ; } ;
const wireType = ( id ) => ( wire . data . types . find ( ( t ) => t . id === id ) || { label : id , color : "#888" } ) ;
const wireNode = ( id ) => wire . data . nodes . find ( ( n ) => n . id === id ) ;
const wireName = ( id ) => ( wireNode ( id ) || { label : id } ) . label ;
// connections; pick a layout. Positions (per layout) and what is hidden are kept in this browser only.
// Everything here is named wd... (a wireLayout already belongs to the Keys screen).
const WD _W = 2100 , WD _H = 1300 , WD _DANTE _X = 1660 ;
const WD _LAYOUTS = [ [ "hub" , "Hub" , "This Mac in the middle, the computers around it, each device beside its machine" ] ,
[ "columns" , "Columns" , "Left to right: what Companion drives, Companion's Mac, the computers, what they talk to" ] ,
[ "groups" , "Groups" , "Machines grouped by kind" ] , [ "free" , "Free" , "Pulled together by their connections" ] ] ;
const wd = { data : null , pos : null , hidden : null , types : null , layout : "hub" , zones : [ ] , sel : null , focus : false , problems : false , drag : null , view : null } ;
const wdRead = ( k , d ) => { try { return JSON . parse ( store . get ( k ) || "null" ) || d ; } catch ( e ) { return d ; } } ;
const wdSave = ( ) => { store . set ( "sp-wd-pos" , JSON . stringify ( wd . pos ) ) ; store . set ( "sp-wd-hidden" , JSON . stringify ( [ ... wd . hidden ] ) ) ; store . set ( "sp-wd-types" , JSON . stringify ( [ ... wd . types ] ) ) ; store . set ( "sp-wd-layout" , wd . layout ) ; } ;
const wdType = ( id ) => ( wd . data . types . find ( ( t ) => t . id === id ) || { label : id , color : "#888" } ) ;
const wdNode = ( id ) => wd . data . nodes . find ( ( n ) => n . id === id ) ;
const wdName = ( id ) => ( wdNode ( id ) || { label : id } ) . label ;
const wdP = ( ) => ( wd . pos [ wd . layout ] = wd . pos [ wd . layout ] || { } ) ;
const wdSize = ( n ) => n . kind === "computer" ? { w : 168 , h : 46 } : { w : Math . min ( 190 , 26 + 6.6 * String ( n . label ) . length ) , h : 26 } ;
const wdViewBox = ( svg ) => svg && svg . setAttribute ( "viewBox" , ` ${ wd . view . x } ${ wd . view . y } ${ wd . view . w } ${ wd . view . h } ` ) ;
async function wiring ( rest , quiet ) {
if ( quiet && ( wire . drag || document . getElementById ( "dialog" ) . hidden === false ) ) return ;
wire . data = await api ( "/api/wiring" ) ;
if ( ! wire . pos ) { wire . pos = wireRead ( "sp-wire-pos" , { } ) ; wire . hidden = new Set ( wireRead ( "sp-wire-hidden" , [ ] ) ) ; wire . types = new Set ( wireRead ( "sp-wire-types" , [ "watch" ] ) ) ; } // the watchdog's own lines: hidden at first
if ( wire . sel && ! ( wire . sel . edge ? wire . data . edges . some ( ( e ) => e . id === wire . sel . id ) : wireNode ( wire . sel . id ) ) ) wire . sel = null ;
wireLayout ( ) ;
if ( ! ( quiet && document . getElementById ( "wiresvg" ) ) ) {
main . innerHTML = ` <h1>Wiring ${ hint ( "What is connected to what. Drag a machine to move it, click it for its scripts and connections. Colours: the kind of connection; red = that connection has a problem; dashed = can't see it from here." ) } </h1>
< div class = "tabs" id = "wiretools" > < / d i v > < d i v c l a s s = " w i r e " > < d i v c l a s s = " w i r e c a n v a s " > < d i v c l a s s = " w z o o m " > < b u t t o n c l a s s = " s m a l l " i d = " w z i n " a r i a - l a b e l = " z o o m i n " > + < / b u t t o n > < b u t t o n c l a s s = " s m a l l " i d = " w z o u t " a r i a - l a b e l = " z o o m o u t " > − < / b u t t o n > < b u t t o n c l a s s = " s m a l l " i d = " w f i t " d a t a - h i n t = " S h o w e v e r y t h i n g t h a t i s s h o w n " > F i t < / b u t t o n > < / d i v > < s v g i d = " w i r e s v g " c l a s s = " c a n v a s " v i e w B o x = " 0 0 $ { W _ W } $ { W _ H } " r o l e = " i m g " a r i a - l a b e l = " w i r i n g d i a g r a m " > < / s v g > < / d i v > < a s i d e c l a s s = " c a r d w i r e s i d e " i d = " w i r e s i d e " > < / a s i d e > < / d i v > ` ;
wireEvents ( ) ;
if ( quiet && ( wd . drag || document . getElementById ( "dialog" ) . hidden === false ) ) return ;
wd . data = await api ( "/api/wiring" ) ;
if ( ! wd . pos ) {
wd . pos = wdRead ( "sp-wd-pos" , { } ) ; wd . hidden = new Set ( wdRead ( "sp-wd-hidden" , [ ] ) ) ; wd . types = new Set ( wdRead ( "sp-wd-types" , [ "watch" ] ) ) ;
wd . layout = WD _LAYOUTS . some ( ( l ) => l [ 0 ] === store . get ( "sp-wd-layout" ) ) ? store . get ( "sp-wd-layout" ) : "hub" ;
}
wireTools ( ) ; wireDraw ( ) ; wireSide ( ) ;
if ( wd . sel && ! ( wd . sel . edge ? wd . data . edges . some ( ( e ) => e . id === wd . sel . id ) : wdNode ( wd . sel . id ) ) ) wd . sel = null ;
wdLayout ( ) ;
if ( ! ( quiet && document . getElementById ( "wdsvg" ) ) ) {
wd . view = null ; // a fresh visit starts with everything in view
main . innerHTML = ` <h1>Wiring ${ hint ( "What is connected to what. Drag a machine to move it, click it for its scripts and connections. Colours: the kind of connection; red = that connection has a problem; dashed = can't see it from here. Two-finger scroll or pinch zooms, drag the background to move around." ) } </h1>
< div class = "tabs" id = "wdtools" > < / d i v > < d i v c l a s s = " w i r e " > < d i v c l a s s = " w i r e c a n v a s " > < d i v c l a s s = " w z o o m " > < b u t t o n c l a s s = " s m a l l " i d = " w d z i n " a r i a - l a b e l = " z o o m i n " > + < / b u t t o n > < b u t t o n c l a s s = " s m a l l " i d = " w d z o u t " a r i a - l a b e l = " z o o m o u t " > − < / b u t t o n > < b u t t o n c l a s s = " s m a l l " i d = " w d f i t " d a t a - h i n t = " S h o w e v e r y t h i n g t h a t i s s h o w n " > F i t < / b u t t o n > < / d i v > < s v g i d = " w d s v g " c l a s s = " c a n v a s " v i e w B o x = " 0 0 $ { W D _ W } $ { W D _ H } " r o l e = " i m g " a r i a - l a b e l = " w i r i n g d i a g r a m " > < / s v g > < / d i v > < a s i d e c l a s s = " c a r d w i r e s i d e " i d = " w d s i d e " > < / a s i d e > < / d i v > ` ;
wdEvents ( ) ;
}
wdTools ( ) ; wdDraw ( ) ; wdSide ( ) ;
}
function wireLayout ( all ) {
const d = wire . data , cx = W _W / 2 , cy = W _H / 2 ;
const fresh = d . nodes . filter ( ( n ) => all || ! wire . pos [ n . id ] ) . map ( ( n ) => n . id ) ;
if ( ! fresh . length ) return ;
const nb = { } ;
for ( const e of d . edges ) if ( e . type !== "watch" ) { ( nb [ e . from ] = nb [ e . from ] || [ ] ) . push ( e . to ) ; ( nb [ e . to ] = nb [ e . to ] || [ ] ) . push ( e . from ) ; }
const move = new Set ( fresh ) , hubs = d . nodes . filter ( ( n ) => ( n . kind === "computer" || n . kind === "switch" ) && n . id !== d . me ) ;
hubs . sort ( ( a , b ) => ( a . kind === "switch" ) - ( b . kind === "switch" ) || a . label . localeCompare ( b . label ) ) ;
if ( d . me && move . has ( d . me ) ) wire . pos [ d . me ] = { x : cx , y : cy } ;
const angle = { } ;
hubs . forEach ( ( n , i ) => { angle [ n . id ] = 2 * Math . PI * i / hubs . length - Math . PI / 2 ; if ( move . has ( n . id ) ) wire . pos [ n . id ] = { x : cx + W _W * 0.24 * Math . cos ( angle [ n . id ] ) , y : cy + W _H * 0.26 * Math . sin ( angle [ n . id ] ) } ; } ) ;
// Each device beside the machine it belongs to: its switch or computer with the most links, else Companion's Mac.
const anchor = { } , groups = { } ;
for ( const n of d . nodes ) {
if ( n . kind === "computer" || n . kind === "switch" ) continue ;
const count = { } ;
for ( const o of nb [ n . id ] || [ ] ) if ( angle [ o ] !== undefined ) count [ o ] = ( count [ o ] || 0 ) + 1 ;
const best = Object . keys ( count ) . sort ( ( a , b ) => count [ b ] - count [ a ] ) [ 0 ] || d . me ;
anchor [ n . id ] = best ; ( groups [ best ] = groups [ best ] || [ ] ) . push ( n ) ;
// ---- layouts: each returns {pos: {id: {x, y}}, zones: [{x, y, w, h, label}]} for every node --------------
function wdGraph ( ) {
const d = wd . data , nb = { } , kinds = { } ;
for ( const e of d . edges ) {
if ( e . type !== "watch" ) { ( nb [ e . from ] = nb [ e . from ] || [ ] ) . push ( e . to ) ; ( nb [ e . to ] = nb [ e . to ] || [ ] ) . push ( e . from ) ; }
for ( const end of [ e . from , e . to ] ) ( kinds [ end ] = kinds [ end ] || new Set ( ) ) . add ( e . type ) ;
}
// Dante is its own network: its switches and the devices only on it sit apart, on the right.
const dante = new Set ( d . nodes . filter ( ( n ) => n . kind === "switch" || ( n . kind !== "computer" && kinds [ n . id ] && [ ... kinds [ n . id ] ] . every ( ( t ) => t === "dante" ) ) ) . map ( ( n ) => n . id ) ) ;
return { nb , dante } ;
}
function wdDanteZone ( g , pos ) {
const d = wd . data , switches = d . nodes . filter ( ( n ) => n . kind === "switch" ) . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) ;
let y = 70 ;
for ( const s of switches ) {
pos [ s . id ] = { x : WD _DANTE _X + 215 , y } ;
const devs = ( g . nb [ s . id ] || [ ] ) . filter ( ( id ) => g . dante . has ( id ) && id !== s . id ) . sort ( ( a , b ) => wdName ( a ) . localeCompare ( wdName ( b ) ) ) ;
devs . forEach ( ( id , i ) => { pos [ id ] = { x : WD _DANTE _X + 110 + ( i % 2 ) * 210 , y : y + 44 + Math . floor ( i / 2 ) * 34 } ; } ) ;
y += 44 + Math . ceil ( devs . length / 2 ) * 34 + 40 ;
}
for ( const id of g . dante ) if ( ! pos [ id ] ) { pos [ id ] = { x : WD _DANTE _X + 215 , y } ; y += 34 ; }
return switches . length ? [ { x : WD _DANTE _X - 10 , y : 20 , w : WD _W - WD _DANTE _X , h : Math . max ( 200 , y - 10 ) , label : "Dante network" } ] : [ ] ;
}
function wdAnchor ( g , n , hubs ) {
const count = { } ;
for ( const o of g . nb [ n . id ] || [ ] ) if ( hubs . has ( o ) ) count [ o ] = ( count [ o ] || 0 ) + 1 ;
return Object . keys ( count ) . sort ( ( a , b ) => count [ b ] - count [ a ] ) [ 0 ] || wd . data . me ;
}
function wdHub ( g ) {
const d = wd . data , pos = { } , cx = WD _DANTE _X / 2 - 20 , cy = WD _H / 2 , rx = WD _DANTE _X * 0.27 , ry = WD _H * 0.27 ;
const hubs = d . nodes . filter ( ( n ) => n . kind === "computer" && n . id !== d . me ) . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) ;
if ( d . me ) pos [ d . me ] = { x : cx , y : cy } ;
const angle = { } ;
hubs . forEach ( ( n , i ) => { angle [ n . id ] = 2 * Math . PI * i / hubs . length - Math . PI / 2 ; pos [ n . id ] = { x : cx + rx * Math . cos ( angle [ n . id ] ) , y : cy + ry * Math . sin ( angle [ n . id ] ) } ; } ) ;
const groups = { } , hubIds = new Set ( hubs . map ( ( n ) => n . id ) ) ;
for ( const n of d . nodes ) if ( n . kind !== "computer" && ! g . dante . has ( n . id ) ) { const a = wdAnchor ( g , n , hubIds ) ; ( groups [ a ] = groups [ a ] || [ ] ) . push ( n ) ; }
for ( const [ hub , list ] of Object . entries ( groups ) ) {
list . sort ( ( a , b ) => String ( a . kind ) . localeCompare ( String ( b . kind ) ) || String ( a . label ) . localeCompare ( String ( b . label ) ) ) ;
list . forEach ( ( n , i ) => {
if ( ! move . has ( n . id ) ) return ;
if ( hub === d . me || angle [ hub ] === undefined ) { // the outer ring, kinds together
const a = 2 * Math . PI * i / list . length - Math . PI / 2 + 0.2 ;
wire . pos [ n . id ] = { x : cx + W _W * 0.43 * Math . cos ( a ) , y : cy + W _H * 0.44 * Math . sin ( a ) } ;
} else { // a fan outward from its machine
const a = angle [ hub ] + ( i - ( list . length - 1 ) / 2 ) * 0.12 , r = 1.0 + 0.16 * ( 1 + ( i % 2 ) ) ;
wire . pos [ n . id ] = { x : cx + W _W * 0.24 * r * Math . cos ( a ) , y : cy + W _H * 0.26 * r * Math . sin ( a ) } ;
}
if ( angle [ hub ] === undefined ) { const a = 2 * Math . PI * i / list . length - Math . PI / 2 + 0.2 ; pos [ n . id ] = { x : cx + rx * 1.75 * Math . cos ( a ) , y : cy + ry * 1.7 * Math . sin ( a ) } ; }
else { const a = angle [ hub ] + ( i - ( list . length - 1 ) / 2 ) * 0.12 , r = 1.16 + 0.16 * ( i % 2 ) ; pos [ n . id ] = { x : cx + rx * r * Math . cos ( a ) , y : cy + ry * r * Math . sin ( a ) } ; }
} ) ;
}
// Push apart whatever overlaps (boxes, with a margin), each device held near its machine.
return { pos , zones : wdDanteZone ( g , pos ) } ;
}
function wdColumns ( g ) {
const d = wd . data , pos = { } ;
const comps = d . nodes . filter ( ( n ) => n . kind === "computer" && n . id !== d . me ) . sort ( ( a , b ) => a . label . localeCompare ( b . label ) ) ;
const hubIds = new Set ( comps . map ( ( n ) => n . id ) ) , mine = [ ] , theirs = { } ;
for ( const n of d . nodes ) {
if ( n . kind === "computer" || g . dante . has ( n . id ) ) continue ;
const a = wdAnchor ( g , n , hubIds ) ;
if ( a === d . me || ! hubIds . has ( a ) ) mine . push ( n ) ; else ( theirs [ a ] = theirs [ a ] || [ ] ) . push ( n ) ;
}
mine . sort ( ( a , b ) => String ( a . kind ) . localeCompare ( String ( b . kind ) ) || String ( a . label ) . localeCompare ( String ( b . label ) ) ) ;
const per = Math . ceil ( mine . length / Math . max ( 1 , Math . ceil ( mine . length / 34 ) ) ) ;
mine . forEach ( ( n , i ) => { pos [ n . id ] = { x : 130 + Math . floor ( i / per ) * 215 , y : 60 + ( i % per ) * 35 } ; } ) ;
const colX = 130 + Math . ceil ( mine . length / per ) * 215 + 140 ;
if ( d . me ) pos [ d . me ] = { x : colX , y : WD _H / 2 } ;
let y = 60 ;
for ( const c of comps ) {
pos [ c . id ] = { x : colX + 330 , y } ;
const list = ( theirs [ c . id ] || [ ] ) . sort ( ( a , b ) => String ( a . label ) . localeCompare ( String ( b . label ) ) ) ;
list . forEach ( ( n , i ) => { pos [ n . id ] = { x : colX + 600 + ( i % 2 ) * 210 , y : y + Math . floor ( i / 2 ) * 34 } ; } ) ;
y += Math . max ( 80 , Math . ceil ( list . length / 2 ) * 34 + 30 ) ;
}
return { pos , zones : [ { x : 20 , y : 20 , w : colX - 150 , h : WD _H - 40 , label : "Companion drives" } ] . concat ( wdDanteZone ( g , pos ) ) } ;
}
function wdGroups ( g ) {
const d = wd . data , pos = { } , zones = [ ] , word = { computer : "Computers" , service : "Services and cloud" , cloud : "Services and cloud" , MIDI : "MIDI devices" , device : "Devices" } ;
const by = { } ;
for ( const n of d . nodes ) if ( ! g . dante . has ( n . id ) ) { const k = word [ n . kind ] || ( n . kind . charAt ( 0 ) . toUpperCase ( ) + n . kind . slice ( 1 ) + "s" ) ; ( by [ k ] = by [ k ] || [ ] ) . push ( n ) ; }
const order = Object . keys ( by ) . sort ( ( a , b ) => ( a === "Computers" ? - 1 : b === "Computers" ? 1 : by [ b ] . length - by [ a ] . length ) ) ;
let x = 30 , y = 30 , rowH = 0 ;
for ( const k of order ) {
const list = by [ k ] . sort ( ( a , b ) => String ( a . label ) . localeCompare ( String ( b . label ) ) ) ;
const isComp = k === "Computers" , cols = isComp ? 2 : Math . min ( 3 , Math . ceil ( list . length / 8 ) ) , cw = isComp ? 190 : 205 , rh = isComp ? 62 : 34 ;
const w = cols * cw + 30 , h = Math . ceil ( list . length / cols ) * rh + 50 ;
if ( x + w > WD _DANTE _X - 20 ) { x = 30 ; y += rowH + 30 ; rowH = 0 ; }
list . forEach ( ( n , i ) => { pos [ n . id ] = { x : x + 15 + cw / 2 + ( i % cols ) * cw , y : y + 45 + Math . floor ( i / cols ) * rh } ; } ) ;
zones . push ( { x , y , w , h , label : k } ) ;
x += w + 30 ; rowH = Math . max ( rowH , h ) ;
}
return { pos , zones : zones . concat ( wdDanteZone ( g , pos ) ) } ;
}
function wdFree ( g ) {
const d = wd . data , base = wdHub ( g ) . pos , pos = { } ;
for ( const id in base ) pos [ id ] = { x : base [ id ] . x + ( Math . random ( ) - 0.5 ) * 40 , y : base [ id ] . y + ( Math . random ( ) - 0.5 ) * 40 } ;
const ids = d . nodes . filter ( ( n ) => ! g . dante . has ( n . id ) ) . map ( ( n ) => n . id ) ;
for ( let it = 0 ; it < 240 ; it ++ ) {
const f = { } ;
for ( const a of ids ) f [ a ] = { x : 0 , y : 0 } ;
for ( const a of ids ) for ( const b of ids ) {
if ( a === b ) continue ;
const dx = pos [ a ] . x - pos [ b ] . x , dy = pos [ a ] . y - pos [ b ] . y , dd = Math . hypot ( dx , dy ) || 1 ;
if ( dd < 220 ) { const k = ( 220 - dd ) / dd * 0.06 ; f [ a ] . x += dx * k ; f [ a ] . y += dy * k ; }
}
for ( const e of d . edges ) if ( pos [ e . from ] && pos [ e . to ] && f [ e . from ] && f [ e . to ] ) {
const dx = pos [ e . to ] . x - pos [ e . from ] . x , dy = pos [ e . to ] . y - pos [ e . from ] . y , dd = Math . hypot ( dx , dy ) || 1 , k = ( dd - 170 ) / dd * 0.02 ;
f [ e . from ] . x += dx * k ; f [ e . from ] . y += dy * k ; f [ e . to ] . x -= dx * k ; f [ e . to ] . y -= dy * k ;
}
for ( const a of ids ) { pos [ a ] . x = Math . max ( 110 , Math . min ( WD _DANTE _X - 110 , pos [ a ] . x + Math . max ( - 20 , Math . min ( 20 , f [ a ] . x ) ) ) ) ; pos [ a ] . y = Math . max ( 30 , Math . min ( WD _H - 30 , pos [ a ] . y + Math . max ( - 20 , Math . min ( 20 , f [ a ] . y ) ) ) ) ; }
}
return { pos , zones : wdDanteZone ( g , pos ) } ;
}
// Places the nodes that have no position in this layout yet (all of them with all=true), then pushes apart what overlaps.
function wdLayout ( all ) {
const d = wd . data , P = wdP ( ) , g = wdGraph ( ) ;
const lay = ( { hub : wdHub , columns : wdColumns , groups : wdGroups , free : wdFree } [ wd . layout ] || wdHub ) ( g ) ;
wd . zones = lay . zones ;
const move = new Set ( d . nodes . filter ( ( n ) => all || ! P [ n . id ] ) . map ( ( n ) => n . id ) ) ;
if ( ! move . size ) return ;
for ( const id of move ) P [ id ] = Object . assign ( { } , lay . pos [ id ] || { x : WD _DANTE _X / 2 , y : WD _H / 2 } ) ;
const ids = d . nodes . map ( ( n ) => n . id ) , size = { } ;
for ( const n of d . nodes ) size [ n . id ] = nodeSize ( n ) ;
for ( let it = 0 ; it < 260 ; it ++ ) {
for ( const n of d . nodes ) size [ n . id ] = wdSize ( n ) ;
for ( let it = 0 ; it < ( wd . layout === "hub" || wd . layout === "free" ? 200 : 40 ) ; it ++ ) {
let moved = false ;
for ( let i = 0 ; i < ids . length ; i ++ ) for ( let j = i + 1 ; j < ids . length ; j ++ ) {
const a = ids [ i ] , b = ids [ j ] ;
if ( ! move . has ( a ) && ! move . has ( b ) ) continue ;
const pa = wire . pos [ a ] , pb = wire . pos [ b ] ;
const ox = ( size [ a ] . w + size [ b ] . w ) / 2 + 14 - Math . abs ( pa . x - pb . x ) , oy = ( size [ a ] . h + size [ b ] . h ) / 2 + 12 - Math . abs ( pa . y - pb . y ) ;
const pa = P [ a ] , pb = P [ b ] ;
const ox = ( size [ a ] . w + size [ b ] . w ) / 2 + 12 - Math . abs ( pa . x - pb . x ) , oy = ( size [ a ] . h + size [ b ] . h ) / 2 + 8 - Math . abs ( pa . y - pb . y ) ;
if ( ox <= 0 || oy <= 0 ) continue ;
moved = true ;
const ma = move . has ( a ) ? ( move . has ( b ) ? 0.5 : 1 ) : 0 , mb = 1 - ma ;
if ( ox < oy ) { const sx = ( pa . x < pb . x ? - 1 : 1 ) * ox ; pa . x += sx * ma ; pb . x -= sx * mb ; }
else { const sy = ( pa . y < pb . y ? - 1 : 1 ) * oy ; pa . y += sy * ma ; pb . y -= sy * mb ; }
}
for ( const id of move ) {
const h = anchor [ id ] , p = wire . pos [ id ] ;
if ( h && h !== d . me && wire . pos [ h ] ) { const q = wire . pos [ h ] , dx = q . x - p . x , dy = q . y - p . y , dd = Math . hypot ( dx , dy ) || 1 ; if ( dd > 230 ) { p . x += dx * 0.02 ; p . y += dy * 0.02 ; } }
p . x = Math . max ( size [ id ] . w / 2 + 6 , Math . min ( W _W - size [ id ] . w / 2 - 6 , p . x ) ) ; p . y = Math . max ( 20 , Math . min ( W _H - 20 , p . y ) ) ;
}
if ( ! moved ) break ;
}
wireSave ( ) ;
wdSave ( ) ;
}
function wireFit ( ) {
const { nodes } = wireShown ( ) ;
if ( ! nodes . length ) { wire . view = { x : 0 , y : 0 , w : W _W , h : W _H } ; return ; }
let x0 = Infinity , y0 = Infinity , x1 = - Infinity , y1 = - Infinity ;
for ( const n of nodes ) { const p = wire . pos [ n . id ] , { w , h } = nodeSize ( n ) ; x0 = Math . min ( x0 , p . x - w / 2 ) ; x1 = Math . max ( x1 , p . x + w / 2 ) ; y0 = Math . min ( y0 , p . y - h / 2 ) ; y1 = Math . max ( y1 , p . y + h / 2 ) ; }
const svg = document . getElementById ( "wiresvg" ) , box = svg && svg . getBoundingClientRect ( ) ;
const pad = 30 , w = x1 - x0 + 2 * pad , h = y1 - y0 + 2 * pad , ratio = box && box . height ? box . width / box . height : W _W / W _H ;
const vw = Math . max ( w , h * ratio ) , vh = vw / ratio ;
wire . view = { x : ( x0 + x1 ) / 2 - vw / 2 , y : ( y0 + y1 ) / 2 - vh / 2 , w : vw , h : vh } ;
}
function wireZoom ( f , px , py ) {
const v = wire . view , nw = Math . max ( 250 , Math . min ( W _W * 2.5 , v . w * f ) ) , k = nw / v . w ;
px = px == null ? v . x + v . w / 2 : px ; py = py == null ? v . y + v . h / 2 : py ;
wire . view = { x : px - ( px - v . x ) * k , y : py - ( py - v . y ) * k , w : nw , h : v . h * k } ;
const svg = document . getElementById ( "wiresvg" ) ; if ( svg ) svg . setAttribute ( "viewBox" , ` ${ wire . view . x } ${ wire . view . y } ${ wire . view . w } ${ wire . view . h } ` ) ;
}
function wireShown ( ) {
const d = wire . data , sel = wire . sel && ! wire . sel . edge ? wire . sel . id : null ;
const edges = d . edges . filter ( ( e ) => ! wire . types . has ( e . type ) && ! wire . hidden . has ( e . id ) && ( ! wire . problems || e . light === "problem" )
&& ( ! wire . focus || ! sel || e . from === sel || e . to === sel ) ) ;
function wdShown ( ) {
const d = wd . data , sel = wd . sel && ! wd . sel . edge ? wd . sel . id : null ;
const edges = d . edges . filter ( ( e ) => ! wd . types . has ( e . type ) && ! wd . hidden . has ( e . id ) && ( ! wd . problems || e . light === "problem" )
&& ( ! wd . focus || ! sel || e . from === sel || e . to === sel ) ) ;
const ends = new Set ( ) ; for ( const e of edges ) { ends . add ( e . from ) ; ends . add ( e . to ) ; }
const nodes = d . nodes . filter ( ( n ) => ends . has ( n . id ) || ( n . kind === "computer" && ! wire . focus && ! wire . problems ) || n . id === sel ) ;
const nodes = d . nodes . filter ( ( n ) => ends . has ( n . id ) || ( n . kind === "computer" && ! wd . focus && ! wd . problems ) || n . id === sel ) ;
return { edges , nodes } ;
}
const nodeSize = ( n ) => n . kind === "computer" ? { w : 168 , h : 46 } : { w : Math . min ( 190 , 26 + 6.6 * String ( n . label ) . length ) , h : 26 } ;
function wdFit ( ) {
const { nodes } = wdShown ( ) , P = wdP ( ) ;
if ( ! nodes . length ) { wd . view = { x : 0 , y : 0 , w : WD _W , h : WD _H } ; return ; }
let x0 = Infinity , y0 = Infinity , x1 = - Infinity , y1 = - Infinity ;
for ( const n of nodes ) { const p = P [ n . id ] , { w , h } = wdSize ( n ) ; if ( ! p ) continue ; x0 = Math . min ( x0 , p . x - w / 2 ) ; x1 = Math . max ( x1 , p . x + w / 2 ) ; y0 = Math . min ( y0 , p . y - h / 2 ) ; y1 = Math . max ( y1 , p . y + h / 2 ) ; }
const svg = document . getElementById ( "wdsvg" ) , box = svg && svg . getBoundingClientRect ( ) ;
const pad = 40 , w = x1 - x0 + 2 * pad , h = y1 - y0 + 2 * pad , ratio = box && box . height ? box . width / box . height : WD _W / WD _H ;
const vw = Math . max ( w , h * ratio ) , vh = vw / ratio ;
wd . view = { x : ( x0 + x1 ) / 2 - vw / 2 , y : ( y0 + y1 ) / 2 - vh / 2 , w : vw , h : vh } ;
}
function wdZoom ( f , px , py ) {
const v = wd . view , nw = Math . max ( 250 , Math . min ( WD _W * 3 , v . w * f ) ) , k = nw / v . w ;
px = px == null ? v . x + v . w / 2 : px ; py = py == null ? v . y + v . h / 2 : py ;
wd . view = { x : px - ( px - v . x ) * k , y : py - ( py - v . y ) * k , w : nw , h : v . h * k } ;
wdViewBox ( document . getElementById ( "wdsvg" ) ) ;
}
function wireDraw ( ) {
const svg = document . getElementById ( "wire svg" ) ;
function wdDraw ( ) {
const svg = document . getElementById ( "wd svg" ) ;
if ( ! svg ) return ;
if ( ! wire . view ) wireFit ( ) ;
svg . setAttribute ( "viewBox" , ` ${ wire . view . x } ${ wire . view . y } ${ wire . view . w } ${ wire . view . h } ` ) ;
const { edges , nodes } = wireShown ( ) , sel = wire . sel ;
const near = new Set ( ) ;
if ( ! wd . view ) wdFit ( ) ;
wdViewBox ( svg ) ;
const P = wdP ( ) , { edges , nodes } = wdShown ( ) , sel = wd . sel , near = new Set ( ) ;
if ( sel && ! sel . edge ) { near . add ( sel . id ) ; for ( const e of edges ) if ( e . from === sel . id || e . to === sel . id ) { near . add ( e . from ) ; near . add ( e . to ) ; } }
if ( sel && sel . edge ) { const e = wire . data . edges . find ( ( x ) => x . id === sel . id ) ; if ( e ) { near . add ( e . from ) ; near . add ( e . to ) ; } }
if ( sel && sel . edge ) { const e = wd . data . edges . find ( ( x ) => x . id === sel . id ) ; if ( e ) { near . add ( e . from ) ; near . add ( e . to ) ; } }
const shownIds = new Set ( nodes . map ( ( n ) => n . id ) ) ;
let out = wd . zones . filter ( ( z ) => z . label !== "Dante network" || nodes . some ( ( n ) => n . kind === "switch" ) )
. map ( ( z ) => ` <g class="wzone"><rect x=" ${ z . x } " y=" ${ z . y } " width=" ${ z . w } " height=" ${ z . h } " rx="14"/><text x=" ${ z . x + 14 } " y=" ${ z . y + 22 } "> ${ esc ( z . label ) } </text></g> ` ) . join ( "" ) ;
const pairs = { } ;
for ( const e of edges ) { const k = [ e . from , e . to ] . sort ( ) . join ( "|" ) ; ( pairs [ k ] = pairs [ k ] || [ ] ) . push ( e ) ; }
let out = "" ;
for ( const list of Object . values ( pairs ) ) list . forEach ( ( e , i ) => {
const a = wire . pos [ e . from ] , b = wire . pos [ e . to ] ;
if ( ! a || ! b ) return ;
const a = P [ e . from ] , b = P [ e . to ] ;
if ( ! a || ! b || ! shownIds . has ( e . from ) || ! shownIds . has ( e . to ) ) return ;
const off = ( i - ( list . length - 1 ) / 2 ) * 16 , dx = b . x - a . x , dy = b . y - a . y , len = Math . hypot ( dx , dy ) || 1 ;
const mx = ( a . x + b . x ) / 2 - dy / len * off , my = ( a . y + b . y ) / 2 + dx / len * off ;
const t = wireType ( e . type ) , on = sel ? ( sel . edge ? sel . id === e . id : e . from === sel . id || e . to === sel . id ) : true ;
out += ` <path class="wedge ${ esc ( e . light ) } ${ on ? "" : " dim" } ${ sel && sel . edge && sel . id === e . id ? " sel" : "" } " data-edge=" ${ esc ( e . id ) } " d="M ${ a . x } , ${ a . y } Q ${ mx } , ${ my } ${ b . x } , ${ b . y } " style="stroke: ${ e . light === "problem" ? "var(--problem)" : t . color } "><title> ${ esc ( t . label ) } : ${ esc ( wireName ( e . from ) ) } ↔ ${ esc ( wireName ( e . to ) ) } · ${ esc ( e . label ) } · ${ esc ( lightWord [ e . light ] || e . light ) } </title></path> ` ;
const t = wdType ( e . type ) , on = sel ? ( sel . edge ? sel . id === e . id : e . from === sel . id || e . to === sel . id ) : true ;
out += ` <path class="wedge ${ esc ( e . light ) } ${ on ? "" : " dim" } ${ sel && sel . edge && sel . id === e . id ? " sel" : "" } " data-edge=" ${ esc ( e . id ) } " d="M ${ a . x } , ${ a . y } Q ${ mx } , ${ my } ${ b . x } , ${ b . y } " style="stroke: ${ e . light === "problem" ? "var(--problem)" : t . color } "><title> ${ esc ( t . label ) } : ${ esc ( wdName ( e . from ) ) } ↔ ${ esc ( wdName ( e . to ) ) } · ${ esc ( e . label ) } · ${ esc ( lightWord [ e . light ] || e . light ) } </title></path> ` ;
} ) ;
for ( const n of nodes ) {
const p = wire . pos [ n . id ] , { w , h } = nodeSize ( n ) , dim = sel && ! near . has ( n . id ) ;
const p = P [ n . id ] ; if ( ! p ) continue ;
const { w , h } = wdSize ( n ) , dim = sel && ! near . has ( n . id ) ;
const scripts = n . scripts ? n . scripts . filter ( ( x ) => x . running ) . length : null ;
const shape = n . kind === "computer" ? ` <rect x=" ${ - w / 2 } " y=" ${ - h / 2 } " width=" ${ w } " height=" ${ h } " rx="9"/> `
: n . kind === "switch" ? ` <rect x=" ${ - w / 2 } " y=" ${ - h / 2 } " width=" ${ w } " height=" ${ h } " rx="2"/> `
: ` <rect x=" ${ - w / 2 } " y=" ${ - h / 2 } " width=" ${ w } " height=" ${ h } " rx=" ${ h / 2 } "/> ` ;
const name = String ( n . label ) . length > 26 ? String ( n . label ) . slice ( 0 , 25 ) + "…" : n . label ;
out += ` <g class="wnode ${ esc ( n . kind ) } ${ esc ( n . light ) } ${ dim ? " dim" : "" } ${ sel && ! sel . edge && sel . id === n . id ? " sel" : "" } " data-node=" ${ esc ( n . id ) } " transform="translate( ${ p . x } , ${ p . y } )"><title> ${ esc ( n . label ) } ${ n . address ? " · " + esc ( n . address ) : "" } ${ n . reason ? " · " + esc ( n . reason ) : "" } </title> ${ shape }
out += ` <g class="wnode ${ esc ( n . kind ) } ${ esc ( n . light ) } ${ dim ? " dim" : "" } ${ sel && ! sel . edge && sel . id === n . id ? " sel" : "" } " data-node=" ${ esc ( n . id ) } " transform="translate( ${ p . x } , ${ p . y } )"><title> ${ esc ( n . label ) } ${ n . address ? " · " + esc ( n . address ) : "" } ${ n . reason ? " · " + esc ( n . reason ) : "" } </title>
< rect x = "${-w / 2}" y = "${-h / 2}" width = "${w}" height = "${h}" rx = "${n.kind === " computer " ? 9 : n.kind === " switch " ? 2 : h / 2}" / >
< circle class = "wdot" cx = "${-w / 2 + 12}" cy = "0" r = "5" / > < text x = "${-w / 2 + 22}" y = "${n.kind === " computer " ? -3 : 4}" > $ { esc ( name ) } < / t e x t >
$ { n . kind === "computer" ? ` <text class="wsub" x=" ${ - w / 2 + 22 } " y="13"> ${ scripts == null ? "scripts: not looked yet" : scripts + " script" + ( scripts === 1 ? "" : "s" ) + " running" } ${ n . self ? " · this Mac" : "" } </text> ` : "" } < / g > ` ;
}
svg . innerHTML = out ;
}
function wireTools ( ) {
const el = document . getElementById ( "wire tools" ) ;
function wdTools ( ) {
const el = document . getElementById ( "wd tools" ) ;
if ( ! el ) return ;
const d = wire . data , count = { } ;
const d = wd . data , count = { } ;
for ( const e of d . edges ) count [ e . type ] = ( count [ e . type ] || 0 ) + 1 ;
const bad = d . edges . filter ( ( e ) => e . light === "problem" ) . length ;
el . innerHTML = d . types . filter ( ( t ) => count [ t . id ] ) . map ( ( t ) => ` <button class="small wtype ${ wire . types . has ( t . id ) ? " off " : " on " } " data-wtype=" ${ esc ( t . id ) } " data-hint=" ${ esc ( t . hint ) } · click to ${ wire . types . has ( t . id ) ? "show" : "hide" } "><i style="background: ${ t . color } "></i> ${ esc ( t . label ) } <span class="muted"> ${ count [ t . id ] } </span></button> ` ) . join ( "" )
+ ` <button class="small ${ wire . problems ? " on " : "" } " id="wprob" data-hint="Only the connections with a problem">Problems only <span class="muted"> ${ bad } </span></button> `
+ ` <button class="small" id="wall" data-hint="Show every connection again">Show all </button> `
+ ` <button class="small" id="wre " data-hint="Forget where machines were dragged and lay them out again">Re-arrange </button> `
+ ` <button class="small" id="wprobe" style="margin-left:auto" ${ d . probing ? "disabled" : "" } data-hint="Log in to every computer now and list its scripts (read-only)"> ${ d . probing ? "Looking…" : "Check scripts now" } </button> ` ;
for ( const b of el . querySelectorAll ( "[data-wtype]" ) ) b . onclick = ( ) => { const t = b . dataset . wtype ; wire . types . has ( t ) ? wire . types . delete ( t ) : wire . types . add ( t ) ; wireSave ( ) ; wireTools ( ) ; wireDraw ( ) ; wireSide ( ) ; } ;
el . querySelector ( "#wprob" ) . onclick = ( ) => { wire . problems = ! wire . problems ; wireTools ( ) ; wireDraw ( ) ; wireSide ( ) ; } ;
el . querySelector ( "#wall" ) . onclick = ( ) => { wire . types . clear ( ) ; wire . hidden . clear ( ) ; wire . focus = false ; wire . problems = false ; wireSave ( ) ; wireTools ( ) ; wireDraw ( ) ; wireSide ( ) ; } ;
el . querySelector ( "#wre" ) . onclick = ( ) => { wireLayout ( true ) ; wireFit ( ) ; wireDraw ( ) ; } ;
el . querySelector ( "#wprobe " ) . onclick = async ( ev ) => { ev . target . disabled = true ; ev . target . textContent = "Looking…" ; try { await post ( "/api/machines/probe" ) ; } catch ( e ) { alertBox ( e . message ) ; } setTimeout ( ( ) => wiring ( [ ] , true ) , 6000 ) ; } ;
const present = d . types . filter ( ( t ) => count [ t . id ] ) , bad = d . edges . filter ( ( e ) => e . light === "problem" ) . length ;
el . innerHTML = ` <span class="seg"> ${ WD _LAYOUTS . map ( ( [ id , label , h ] ) => ` <button class="small ${ wd . layout === id ? " on " : "" } " data-wdlayout=" ${ id } " data-hint=" ${ esc ( h ) } "> ${ label } </button> ` ) . join ( "" ) } </span> `
+ present . map ( ( t ) => ` <button class="small wtype ${ wd . types . has ( t . id ) ? " off " : " on " } " data-wdtype=" ${ esc ( t . id ) } " data-hint=" ${ esc ( t . hint ) } · click to ${ wd . types . has ( t . id ) ? "show" : "hide" } "><i style="background: ${ t . color } "></i> ${ esc ( t . label ) } <span class="muted"> ${ count [ t . id ] } </span></button> ` ) . join ( "" )
+ ` <button class="small" id="wd all" data-hint="Show every kind of connection (and any you hid one by one)">All</button><button class="small" id="wdnone" data-hint="Hide every kind: then click the one you want">None </button> `
+ ` <button class="small ${ wd . problems ? " on" : "" } " id="wdprob " data-hint="Only the connections with a problem">Problems only <span class="muted"> ${ bad } </span> </button>`
+ ` <button class="small" id="wdre" data-hint="Forget where machines were dragged in this layout and lay them out again">Re-arrange </button> `
+ ` <button class="small" id="wdprobe" style="margin-left:auto" ${ d . probing ? "disabled" : "" } data-hint="Log in to every computer now and list its scripts (read-only)"> ${ d . probing ? "Looking…" : "Check scripts now" } </button> ` ;
const redraw = ( ) => { wdSave ( ) ; wdTools ( ) ; wdDraw ( ) ; wdSide ( ) ; } ;
for ( const b of el . querySelectorAll ( "[data-wdlayout]" ) ) b . onclick = ( ) => { wd . layout = b . dataset . wdlayout ; wdLayout ( ) ; wdFit ( ) ; redraw ( ) ; } ;
for ( const b of el . querySelectorAll ( "[data-wdtype]" ) ) b . onclick = ( ) => { const t = b . dataset . wdtype ; wd . types . has ( t ) ? wd . types . delete ( t ) : wd . types . add ( t ) ; redraw ( ) ; } ;
el . querySelector ( "#wdall " ) . onclick = ( ) => { wd . types . clear ( ) ; wd . hidden . clear ( ) ; wd . focus = false ; wd . problems = false ; redraw ( ) ; } ;
el . querySelector ( "#wdnone" ) . onclick = ( ) => { wd . types = new Set ( present . map ( ( t ) => t . id ) ) ; wd . focus = false ; redraw ( ) ; } ;
el . querySelector ( "#wdprob" ) . onclick = ( ) => { wd . problems = ! wd . problems ; redraw ( ) ; } ;
el . querySelector ( "#wdre" ) . onclick = ( ) => { wdLayout ( true ) ; wdFit ( ) ; redraw ( ) ; } ;
el . querySelector ( "#wdprobe" ) . onclick = async ( ev ) => { ev . target . disabled = true ; ev . target . textContent = "Looking…" ; try { await post ( "/api/machines/probe" ) ; } catch ( e ) { alertBox ( e . message ) ; } setTimeout ( ( ) => wiring ( [ ] , true ) , 6000 ) ; } ;
}
function wireSide ( ) {
const el = document . getElementById ( "wire side" ) ;
function wdSide ( ) {
const el = document . getElementById ( "wd side" ) ;
if ( ! el ) return ;
const d = wire . data , sel = wire . sel ;
const row = ( e , other ) => ` <li><label class="wrow"><input type="checkbox" data-whide=" ${ esc ( e . id ) } " ${ wire . hidden . has ( e . id ) ? "" : "checked" } > ${ lightDot ( e . light , lightWord [ e . light ] ) } <span><b> ${ esc ( wireName ( other ) ) } </b> <span class="muted small"> ${ esc ( e . label ) } </span></span></label></li> ` ;
const d = wd . data , sel = wd . sel ;
const row = ( e , other ) => ` <li><label class="wrow"><input type="checkbox" data-wd hide=" ${ esc ( e . id ) } " ${ wd . hidden . has ( e . id ) ? "" : "checked" } > ${ lightDot ( e . light , lightWord [ e . light ] ) } <span><b> ${ esc ( wdName ( other ) ) } </b> <span class="muted small"> ${ esc ( e . label ) } </span></span></label></li> ` ;
let html ;
if ( sel && sel . edge ) {
const e = d . edges . find ( ( x ) => x . id === sel . id ) , t = wireType ( e . type ) ;
html = ` <h2><i class="wsw" style="background: ${ t . color } "></i> ${ esc ( t . label ) } </h2><p> ${ esc ( wireName ( e . from ) ) } ↔ ${ esc ( wireName ( e . to ) ) } </p><p class="small"> ${ esc ( e . label ) } · ${ lightDot ( e . light ) } ${ esc ( lightWord [ e . light ] || e . light ) } </p>
< p > < button class = "small" data - whide - one = "${esc(e.id)}" > $ { wire . hidden . has ( e . id ) ? "Show" : "Hide" } this connection < / b u t t o n > < b u t t o n c l a s s = " s m a l l " d a t a - w s e l = " $ { e s c ( e . f r o m ) } " > $ { e s c ( w i r e N a m e ( e . f r o m ) ) } < / b u t t o n > < b u t t o n c l a s s = " s m a l l " d a t a - w s e l = " $ { e s c ( e . t o ) } " > $ { e s c ( w i r e N a m e ( e . t o ) ) } < / b u t t o n > < / p > ` ;
const e = d . edges . find ( ( x ) => x . id === sel . id ) , t = wdType ( e . type ) ;
html = ` <h2><i class="wsw" style="background: ${ t . color } "></i> ${ esc ( t . label ) } </h2><p> ${ esc ( wdName ( e . from ) ) } ↔ ${ esc ( wdName ( e . to ) ) } </p><p class="small"> ${ esc ( e . label ) } · ${ lightDot ( e . light ) } ${ esc ( lightWord [ e . light ] || e . light ) } </p> ${ e . detail ? ` <p class="small muted"> ${ esc ( e . detail ) } </p> ` : "" }
< p > < button class = "small" data - wdhide - one = "${esc(e.id)}" > $ { wd . hidden . has ( e . id ) ? "Show" : "Hide" } this connection < / b u t t o n > < b u t t o n c l a s s = " s m a l l " d a t a - w d s e l = " $ { e s c ( e . f r o m ) } " > $ { e s c ( w d N a m e ( e . f r o m ) ) } < / b u t t o n > < b u t t o n c l a s s = " s m a l l " d a t a - w d s e l = " $ { e s c ( e . t o ) } " > $ { e s c ( w d N a m e ( e . t o ) ) } < / b u t t o n > < / p > ` ;
} else if ( sel ) {
const n = wireNode ( sel . id ) , mine = d . edges . filter ( ( e ) => e . from === n . id || e . to === n . id ) ;
const byType = { } ;
const n = wdNode ( sel . id ) , mine = d . edges . filter ( ( e ) => e . from === n . id || e . to === n . id ) , byType = { } ;
for ( const e of mine ) ( byType [ e . type ] = byType [ e . type ] || [ ] ) . push ( e ) ;
const scripts = n . kind !== "computer" ? "" : n . scripts == null
? ` <h3>Scripts</h3><p class="small muted">Not looked yet: press Check scripts now.</p> `
: ` <h3>Scripts <span class="muted small"> ${ n . scripts . filter ( ( x ) => x . running ) . length } running ${ n . scripts . some ( ( x ) => ! x . running ) ? ` , ${ n . scripts . filter ( ( x ) => ! x . running ) . length } stopped ` : "" } </span></h3> ${ n . scripts . length ? ` <ul class="wlist"> ${ n . scripts . map ( ( x ) => ` <li> ${ lightDot ( x . running ? "ok" : "not_needed" , x . running ? "running" : ( x . state || "not running" ) ) } <span class="mono small"> ${ esc ( x . name ) } </span> ${ x . by && x . by !== x . name ? ` <span class="muted small"> ${ esc ( x . by . replace ( /^org\.waterschurch\./ , "" ) ) } </span> ` : "" } </li> ` ) . join ( "" ) } </ul> ` : ` <p class="small muted">None.</p> ` } ${ n . looked ? ` <p class="small muted">looked ${ esc ( ago ( Date . now ( ) / 1000 - n . looked ) ) } </p> ` : "" } ` ;
html = ` <h2> ${ lightDot ( n . light , lightWord [ n . light ] ) } ${ esc ( n . label ) } </h2><p class="small muted"> ${ esc ( n . kind === "computer" ? ( n . os === "windows" ? "Windows PC" : "Mac" ) + ( n . self ? " · this Mac ( the panel) " : "" ) : n . kind ) } ${ n . address ? " · " + esc ( n . address ) : "" } ${ n . role ? " · " + esc ( n . role ) : "" } </p>
: ` <h3>Scripts <span class="muted small"> ${ n . scripts . filter ( ( x ) => x . running ) . length } running ${ n . scripts . some ( ( x ) => ! x . running ) ? ` , ${ n . scripts . filter ( ( x ) => ! x . running ) . length } stopped ` : "" } </span></h3> ${ n . scripts . length ? ` <ul class="wlist"> ${ n . scripts . map ( ( x ) => ` <li> ${ lightDot ( x . running ? "ok" : "not_needed" , x . running ? "running" : ( x . state || "not running" ) ) } <span class="mono small"> ${ esc ( x . name ) } </span> ${ x . by && x . by . replace ( /^org\.waterschurch\./ , "" ) !== x . name ? ` <span class="muted small"> ${ esc ( x . by . replace ( /^org\.waterschurch\./ , "" ) ) } </span> ` : "" } </li> ` ) . join ( "" ) } </ul> ` : ` <p class="small muted">None.</p> ` } ${ n . looked ? ` <p class="small muted">looked ${ esc ( ago ( Date . now ( ) / 1000 - n . looked ) ) } </p> ` : "" } ` ;
html = ` <h2> ${ lightDot ( n . light , lightWord [ n . light ] ) } ${ esc ( n . label ) } </h2><p class="small muted"> ${ esc ( n . kind === "computer" ? ( n . os === "windows" ? "Windows PC" : "Mac" ) + ( n . self ? " · runs the panel" : "" ) : n . kind ) } ${ n . address ? " · " + esc ( n . address ) : "" } ${ n . role ? " · " + esc ( n . role ) : "" } </p>
$ { n . reason ? ` <p class="small"> ${ esc ( n . reason ) } </p> ` : "" } $ { n . names && n . names . length > 1 ? ` <p class="small muted">also called ${ esc ( n . names . filter ( ( x ) => x !== n . label ) . join ( ", " ) ) } </p> ` : "" }
< p > < button class = "small${wire .focus ? " on " : " "}" id = "wfocus" > $ { wire . focus ? "Show everything" : "Only this one's connections" } < / b u t t o n > < / p > $ { s c r i p t s }
< h3 > Connections < span class = "muted small" > $ { mine . length } < / s p a n > < / h 3 > $ { O b j e c t . e n t r i e s ( b y T y p e ) . m a p ( ( [ t , l i s t ] ) = > ` < p c l a s s = " s m a l l w t y p " > < i c l a s s = " w s w " s t y l e = " b a c k g r o u n d : $ { w i r e T y p e ( t ) . c o l o r } " > < / i > $ { e s c ( w i r e T y p e ( t ) . l a b e l ) } < / p > < u l c l a s s = " w l i s t " > $ { l i s t . m a p ( ( e ) = > r o w ( e , e . f r o m = = = n . i d ? e . t o : e . f r o m ) ) . j o i n ( " " ) } < / u l > ` ) . j o i n ( " " ) | | ` < p c l a s s = " s m a l l m u t e d " > N o n e . < / p > ` }
< p > < button class = "small${wd .focus ? " on " : " "}" id = "wd focus" > $ { wd . focus ? "Show everything" : "Only this one's connections" } < / b u t t o n > < / p > $ { s c r i p t s }
< h3 > Connections < span class = "muted small" > $ { mine . length } < / s p a n > < / h 3 > $ { O b j e c t . e n t r i e s ( b y T y p e ) . m a p ( ( [ t , l i s t ] ) = > ` < p c l a s s = " s m a l l w t y p " > < i c l a s s = " w s w " s t y l e = " b a c k g r o u n d : $ { w d T y p e ( t ) . c o l o r } " > < / i > $ { e s c ( w d T y p e ( t ) . l a b e l ) } < / p > < u l c l a s s = " w l i s t " > $ { l i s t . m a p ( ( e ) = > r o w ( e , e . f r o m = = = n . i d ? e . t o : e . f r o m ) ) . j o i n ( " " ) } < / u l > ` ) . j o i n ( " " ) | | ` < p c l a s s = " s m a l l m u t e d " > N o n e . < / p > ` }
$ { ( n . local || [ ] ) . length ? ` <p class="small muted">On this machine itself: ${ esc ( n . local . map ( ( x ) => x . label ) . join ( ", " ) ) } </p> ` : "" } ` ;
} else {
const comps = d . nodes . filter ( ( n ) => n . kind === "computer" ) ;
const bad = d . edges . filter ( ( e ) => e . light === "problem" ) ;
const comps = d . nodes . filter ( ( n ) => n . kind === "computer" ) , bad = d . edges . filter ( ( e ) => e . light === "problem" ) ;
html = ` <h2> ${ comps . length } computers, ${ d . nodes . length - comps . length } devices</h2><p class="small"> ${ d . edges . length } connections. Click a machine for its scripts and connections; untick a connection to hide it.</p>
< h3 > Scripts per computer < / h 3 > < u l c l a s s = " w l i s t " > $ { c o m p s . m a p ( ( n ) = > ` < l i > < a h r e f = " # " d a t a - w s e l = " $ { e s c ( n . i d ) } " > $ { e s c ( n . l a b e l ) } < / a > < s p a n c l a s s = " m u t e d s m a l l " > $ { n . s c r i p t s = = n u l l ? " n o t l o o k e d y e t " : n . s c r i p t s . f i l t e r ( ( x ) = > x . r u n n i n g ) . l e n g t h + " r u n n i n g " } < / s p a n > < / l i > ` ) . j o i n ( " " ) } < / u l >
$ { bad . length ? ` <h3>Problems <span class="muted small"> ${ bad . length } </span></h3><ul class="wlist"> ${ bad . map ( ( e ) => ` <li><a href="#" data-wedge=" ${ esc ( e . id ) } "> ${ esc ( wireName ( e . from ) ) } ↔ ${ esc ( wireName ( e . to ) ) } </a> <span class="muted small"> ${ esc ( e . label ) } </span></li> ` ) . join ( "" ) } </ul> ` : "" }
$ { wire . hidden . size ? ` <h3>Hidden <span class="muted small"> ${ wire . hidden . size } </span></h3><ul class="wlist"> ${ [ ... wire . hidden ] . map ( ( id ) => d . edges . find ( ( e ) => e . id === id ) ) . filter ( Boolean ) . map ( ( e ) => ` <li><label class="wrow"><input type="checkbox" data-whide=" ${ esc ( e . id ) } "><span> ${ esc ( wireName ( e . from ) ) } ↔ ${ esc ( wireName ( e . to ) ) } <span class="muted small"> ${ esc ( e . label ) } </span></span></label></li> ` ) . join ( "" ) } </ul> ` : "" } ` ;
< h3 > Scripts per computer < / h 3 > < u l c l a s s = " w l i s t " > $ { c o m p s . m a p ( ( n ) = > ` < l i > < a h r e f = " # " d a t a - w d s e l = " $ { e s c ( n . i d ) } " > $ { e s c ( n . l a b e l ) } < / a > < s p a n c l a s s = " m u t e d s m a l l " > $ { n . s c r i p t s = = n u l l ? " n o t l o o k e d y e t " : n . s c r i p t s . f i l t e r ( ( x ) = > x . r u n n i n g ) . l e n g t h + " r u n n i n g " } < / s p a n > < / l i > ` ) . j o i n ( " " ) } < / u l >
$ { bad . length ? ` <h3>Problems <span class="muted small"> ${ bad . length } </span></h3><ul class="wlist"> ${ bad . map ( ( e ) => ` <li><a href="#" data-wd edge=" ${ esc ( e . id ) } "> ${ esc ( wdName ( e . from ) ) } ↔ ${ esc ( wdName ( e . to ) ) } </a> <span class="muted small"> ${ esc ( e . label ) } </span></li> ` ) . join ( "" ) } </ul> ` : "" }
$ { wd . hidden . size ? ` <h3>Hidden <span class="muted small"> ${ wd . hidden . size } </span></h3><ul class="wlist"> ${ [ ... wd . hidden ] . map ( ( id ) => d . edges . find ( ( e ) => e . id === id ) ) . filter ( Boolean ) . map ( ( e ) => ` <li><label class="wrow"><input type="checkbox" data-wd hide=" ${ esc ( e . id ) } "><span> ${ esc ( wdName ( e . from ) ) } ↔ ${ esc ( wdName ( e . to ) ) } <span class="muted small"> ${ esc ( e . label ) } </span></span></label></li> ` ) . join ( "" ) } </ul> ` : "" } ` ;
}
el . innerHTML = html ;
for ( const c of el . querySelectorAll ( "[data-whide]" ) ) c . onchange = ( ) => { c . checked ? wire . hidden . delete ( c . dataset . whide ) : wire . hidden . add ( c . dataset . whide ) ; wireSave ( ) ; wireDraw ( ) ; wireSide ( ) ; } ;
for ( const b of el . querySelectorAll ( "[data-whide-one ]" ) ) b . onclick = ( ) => { const id = b . dataset . whideOne ; wire . hidden . has ( id ) ? wire . hidden . delete ( id ) : wire . hidden . add ( id ) ; wireSave ( ) ; wireDraw ( ) ; wireSide ( ) ; } ;
for ( const a of el . querySelectorAll ( "[data-wsel ]" ) ) a . onclick = ( ev ) => { ev . preventDefault ( ) ; wire . sel = { id : a . dataset . wsel } ; wireDraw ( ) ; wireSide ( ) ; } ;
for ( const a of el . querySelectorAll ( "[data-wedge ]" ) ) a . onclick = ( ev ) => { ev . preventDefault ( ) ; wire . sel = { id : a . dataset . wedge , edge : true } ; wireDraw ( ) ; wireSide ( ) ; } ;
const f = el . querySelector ( "#wfocus" ) ; if ( f ) f . onclick = ( ) => { wire . focus = ! wire . focus ; wireDraw ( ) ; wireSide ( ) ; } ;
const again = ( ) => { wdSave ( ) ; wdDraw ( ) ; wdSide ( ) ; } ;
for ( const c of el . querySelectorAll ( "[data-wd hide]" ) ) c . onchange = ( ) => { c . checked ? wd . hidden . delete ( c . dataset . wdhide ) : wd . hidden . add ( c . dataset . wdhide ) ; again ( ) ; } ;
for ( const b of el . querySelectorAll ( "[data-wdhide-one ]" ) ) b . onclick = ( ) => { const id = b . dataset . wdhideOne ; wd . hidden . has ( id ) ? wd . hidden . delete ( id ) : wd . hidden . add ( id ) ; again ( ) ; } ;
for ( const a of el . querySelectorAll ( "[data-wdsel ]" ) ) a . onclick = ( ev ) => { ev . preventDefault ( ) ; wd . sel = { id : a . dataset . wdsel } ; wdDraw ( ) ; wdSide ( ) ; } ;
for ( const a of el . querySelectorAll ( "[data-wdedge]" ) ) a . onclick = ( ev ) => { ev . preventDefault ( ) ; wd . sel = { id : a . dataset . wdedge , edge : true } ; wdDraw ( ) ; wdSide ( ) ; } ;
const f = el . querySelector ( "#wdfocus" ) ; if ( f ) f . onclick = ( ) => { wd . focus = ! wd . focus ; wdDraw ( ) ; wdSide ( ) ; } ;
}
function wireEvents ( ) {
const svg = document . getElementById ( "wire svg" ) ;
function wdEvents ( ) {
const svg = document . getElementById ( "wd svg" ) ;
const at = ( ev ) => { const pt = svg . createSVGPoint ( ) ; pt . x = ev . clientX ; pt . y = ev . clientY ; return pt . matrixTransform ( svg . getScreenCTM ( ) . inverse ( ) ) ; } ;
document . getElementById ( "wzin" ) . onclick = ( ) => wireZoom ( 0.8 ) ;
document . getElementById ( "wzout" ) . onclick = ( ) => wireZoom ( 1.25 ) ;
document . getElementById ( "wfit" ) . onclick = ( ) => { wireFit ( ) ; wireDraw ( ) ; } ;
svg . addEventListener ( "wheel" , ( ev ) => { ev . preventDefault ( ) ; const p = at ( ev ) ; wireZoom ( ev . deltaY > 0 ? 1.12 : 1 / 1.12 , p . x , p . y ) ; } , { passive : false } ) ;
svg . addEventListener ( "po int erdown" , ( ev ) => {
const g = ev . target . closest ( "[data-node]" ) , path = ev . target . closest ( "[data-edge]" ) ;
document . getElementById ( "wd zin" ) . onclick = ( ) => wdZoom ( 0.8 ) ;
document . getElementById ( "wd zout" ) . onclick = ( ) => wdZoom ( 1.25 ) ;
document . getElementById ( "wd fit" ) . onclick = ( ) => { wdFit ( ) ; wdDraw ( ) ; } ;
// Trackpads send many small wheel steps (pinch: with ctrlKey): each zooms a little, in proportion; a mouse
// wheel's big steps are capped. Sideways two-f ing er scroll moves the view.
svg . addEventListener ( "wheel" , ( ev ) => {
ev . preventDefault ( ) ;
const unit = ev . deltaMode === 1 ? 16 : ev . deltaMode === 2 ? 400 : 1 , dx = ev . deltaX * unit , dy = ev . deltaY * unit ;
if ( ! ev . ctrlKey && Math . abs ( dx ) > Math . abs ( dy ) ) {
const k = wd . view . w / svg . getBoundingClientRect ( ) . width ;
wd . view . x += dx * k ; wdViewBox ( svg ) ; return ;
}
const p = at ( ev ) ;
if ( g ) { const id = g . dataset . node ; wire . drag = { id , dx : wire . pos [ id ] . x - p . x , dy : wire . pos [ id ] . y - p . y , x0 : ev . clientX , y0 : ev . clientY , moved : false } ; }
else if ( path ) { wire . sel = { id : path . dataset . edge , edge : true } ; wireDraw ( ) ; wireSide ( ) ; return ; }
else wire . drag = { pan : true , x0 : ev . clientX , y0 : ev . clientY , vx : wire . view . x , vy : wire . view . y , k : wire . view . w / svg . getBoundingClientRect ( ) . width , moved : false } ;
svg . setPointerCapture ( ev . pointerId ) ; ev . preventDefault ( ) ;
wdZoom ( Math . exp ( Math . max ( - 0.2 , Math . min ( 0.2 , dy * ( ev . ctrlKey ? 0.008 : 0.0018 ) ) ) ) , p . x , p . y ) ;
} , { passive : false } ) ;
svg . addEventListener ( "pointerdown" , ( ev ) => {
const g = ev . target . closest ( "[data-node]" ) , path = ev . target . closest ( "[data-edge]" ) , p = at ( ev ) , P = wdP ( ) ;
if ( g ) { const id = g . dataset . node ; wd . drag = { id , dx : P [ id ] . x - p . x , dy : P [ id ] . y - p . y , x0 : ev . clientX , y0 : ev . clientY , moved : false } ; }
else if ( path ) { wd . sel = { id : path . dataset . edge , edge : true } ; wdDraw ( ) ; wdSide ( ) ; return ; }
else wd . drag = { pan : true , x0 : ev . clientX , y0 : ev . clientY , vx : wd . view . x , vy : wd . view . y , k : wd . view . w / svg . getBoundingClientRect ( ) . width , moved : false } ;
try { svg . setPointerCapture ( ev . pointerId ) ; } catch ( e ) { /* a synthetic pointer */ }
ev . preventDefault ( ) ;
} ) ;
svg . addEventListener ( "pointermove" , ( ev ) => {
const dr = wire . drag ; if ( ! dr ) return ;
const dr = wd . drag ; if ( ! dr ) return ;
if ( ! dr . moved && Math . hypot ( ev . clientX - dr . x0 , ev . clientY - dr . y0 ) < 4 ) return ;
dr . moved = true ;
if ( dr . pan ) { wire . view . x = dr . vx - ( ev . clientX - dr . x0 ) * dr . k ; wire . view . y = dr . vy - ( ev . clientY - dr . y0 ) * dr . k ; svg . setAttribute ( "viewBox" , ` ${ wire . view . x } ${ wire . view . y } ${ wire . view . w } ${ wire . view . h } ` ) ; return ; }
if ( dr . pan ) { wd . view . x = dr . vx - ( ev . clientX - dr . x0 ) * dr . k ; wd . view . y = dr . vy - ( ev . clientY - dr . y0 ) * dr . k ; wdViewBox ( svg ) ; return ; }
const p = at ( ev ) ;
wire . pos [ dr . id ] = { x : p . x + dr . dx , y : p . y + dr . dy } ;
wireDraw ( ) ;
wdP ( ) [ dr . id ] = { x : p . x + dr . dx , y : p . y + dr . dy } ;
wdDraw ( ) ;
} ) ;
const end = ( ) => {
const dr = wire . drag ; wire . drag = null ; if ( ! dr ) return ;
if ( dr . pan ) { if ( ! dr . moved ) { wire . sel = null ; wire . focus = false ; wireDraw ( ) ; wireSide ( ) ; } return ; }
if ( dr . moved ) wireSave ( ) ; else { wire . sel = wire . sel && ! wire . sel . edge && wire . sel . id === dr . id ? null : { id : dr . id } ; if ( ! wire . sel ) wire . focus = false ; }
wireDraw ( ) ; wireSide ( ) ;
const dr = wd . drag ; wd . drag = null ; if ( ! dr ) return ;
if ( dr . pan ) { if ( ! dr . moved ) { wd . sel = null ; wd . focus = false ; wdDraw ( ) ; wdSide ( ) ; } return ; }
if ( dr . moved ) wdSave ( ) ; else { wd . sel = wd . sel && ! wd . sel . edge && wd . sel . id === dr . id ? null : { id : dr . id } ; if ( ! wd . sel ) wd . focus = false ; }
wdDraw ( ) ; wdSide ( ) ;
} ;
svg . addEventListener ( "pointerup" , end ) ; svg . addEventListener ( "pointercancel" , end ) ;
}
@@ -1148,7 +1249,7 @@
else if ( which === "outlets" ) body = await outletsHtml ( ) ;
else if ( which === "companion" ) body = ( await namesHtml ( ) ) + ( await companionHtml ( ) ) ;
else if ( which === "network" ) body = fieldsHtml ( "Network & Dante" ) ;
else if ( which === "setup" ) body = await setupHtml ( c ) ;
else if ( which === "setup" ) body = await siteSetupHtml ( c ) ;
else if ( which === "site" ) body = siteHtml ( c ) ;
else if ( which === "program" ) body = await programHtml ( ) ;
else if ( which === "advanced" ) body = advancedHtml ( ) ;
@@ -1176,7 +1277,7 @@
return screens . map ( ( s ) => { const h = fieldsHtml ( s , true ) ; return h ? ` <h3> ${ esc ( s ) } </h3> ${ h } ` : "" ; } ) . join ( "" ) || "<p class='muted'>Nothing.</p>" ;
}
// Auto-setup: what was found and added, the join lines, and the install line for another location.
async function setupHtml ( c ) {
async function siteSetupHtml ( c ) {
const d = await api ( "/api/discover" ) . catch ( ( ) => null ) ;
if ( ! d ) return "<p class='muted'>Auto-setup is not answering.</p>" ;
const last = d . last || { } , inv = last . inventory || { } ;