const EDGES = [
{from:'CLOSED',to:'LISTEN',label:'passive open\nlisten()'},
{from:'CLOSED',to:'SYN_SENT',label:'active open\nconnect()'},
{from:'LISTEN',to:'SYN_RCVD',label:'rcv SYN\nsnd SYN-ACK'},
{from:'SYN_SENT',to:'ESTABLISHED',label:'rcv SYN-ACK\nsnd ACK'},
{from:'SYN_SENT',to:'SYN_RCVD',label:'rcv SYN\n(simultaneous)'},
{from:'SYN_RCVD',to:'ESTABLISHED',label:'rcv ACK'},
{from:'SYN_RCVD',to:'FIN_WAIT_1',label:'close()\nsnd FIN'},
{from:'ESTABLISHED',to:'FIN_WAIT_1',label:'close()\nsnd FIN'},
{from:'ESTABLISHED',to:'CLOSE_WAIT',label:'rcv FIN\nsnd ACK'},
{from:'FIN_WAIT_1',to:'FIN_WAIT_2',label:'rcv ACK'},
{from:'FIN_WAIT_1',to:'CLOSING',label:'rcv FIN\nsnd ACK'},
{from:'FIN_WAIT_1',to:'TIME_WAIT',label:'rcv FIN+ACK\nsnd ACK'},
{from:'FIN_WAIT_2',to:'TIME_WAIT',label:'rcv FIN\nsnd ACK'},
{from:'CLOSE_WAIT',to:'LAST_ACK',label:'close()\nsnd FIN'},
{from:'CLOSING',to:'TIME_WAIT',label:'rcv ACK'},
{from:'LAST_ACK',to:'CLOSED',label:'rcv ACK'},
{from:'TIME_WAIT',to:'CLOSED',label:'2×MSL\ntimeout'},
];
const positions = {
CLOSED: {x:400,y:50}, LISTEN: {x:150,y:150}, SYN_SENT: {x:650,y:150},
SYN_RCVD: {x:150,y:280}, ESTABLISHED: {x:400,y:280},
FIN_WAIT_1: {x:650,y:280}, CLOSE_WAIT: {x:150,y:410},
FIN_WAIT_2: {x:650,y:410}, CLOSING: {x:500,y:410},
LAST_ACK: {x:150,y:540}, TIME_WAIT: {x:650,y:540},
};
const cy = cytoscape({
container: document.getElementById('cy'),
elements: [
...Object.keys(STATE_DATA).map(id => ({
data: { id, label: id.replace(/_/g,' ') },
position: positions[id] || {x:400,y:300},
})),
...EDGES.map((e,i) => ({
data: { id:`e${i}`, source:e.from, target:e.to, label:e.label },
})),
],
style: [
{ selector: 'node', style: {
'label': 'data(label)', 'text-wrap': 'wrap',
'width': 110, 'height': 44,
'shape': 'roundrectangle',
'background-color': ele => STATE_DATA[ele.id()]?.color || '#0f3460',
'color': '#fff', 'font-size': 11, 'font-weight': 'bold',
'text-valign': 'center', 'text-halign': 'center',
'border-width': 2, 'border-color': '#fff',
}},
{ selector: 'node:selected', style: {
'border-color': '#ffd200', 'border-width': 3,
'box-shadow': '0 0 8px #ffd200',
}},
{ selector: 'edge', style: {
'label': 'data(label)', 'curve-style': 'bezier',
'target-arrow-shape': 'triangle', 'arrow-scale': 1.2,
'line-color': '#667', 'target-arrow-color': '#667',
'font-size': 9, 'text-wrap': 'wrap',
'text-background-color': '#fff', 'text-background-opacity': 0.85,
'text-background-padding': 2,
'color': '#333', 'width': 1.5,
}},
],
layout: { name: 'preset' },
userZoomingEnabled: true,
userPanningEnabled: true,
});
cy.on('tap', 'node', function(evt) {
const id = evt.target.id();
const d = STATE_DATA[id];
if (!d) return;
const diag = d.diagnosis ? `
Diagnosis: ${d.diagnosis}
` : '';
document.getElementById('detailContent').innerHTML = `
${id.replace(/_/g,' ')}
Description:${d.desc}
Trigger:${d.event}
Packets:${d.packet}
Timer:${d.timer}
${diag}`;
});
cy.fit(cy.nodes(), 30);