.calc-header {
margin-bottom: 30px;
padding: 25px;
background: linear-gradient(135deg, #FF6B6B 0%, #C92A2A 100%);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
color: white;
}
.calc-header h2 {
margin-top: 0;
color: white;
font-size: 28px;
margin-bottom: 10px;
}
.calc-header p {
margin: 10px 0 20px 0;
opacity: 0.95;
font-size: 15px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-top: 20px;
}
.form-grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 15px;
margin-top: 15px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: 600;
font-size: 14px;
color: white;
}
.form-group input,
.form-group select {
padding: 12px;
font-size: 15px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 6px;
background: rgba(255,255,255,0.95);
transition: all 0.3s ease;
font-family: 'Courier New', monospace;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #4CAF50;
background: white;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.15);
}
.form-group select {
cursor: pointer;
font-family: inherit;
}
.submit-btn {
padding: 14px 35px;
font-size: 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
margin-top: 15px;
width: 100%;
}
.submit-btn:hover {
background: #45a049;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.submit-btn:active {
background: #3d8b40;
transform: translateY(0);
}
.secondary-btn {
padding: 10px 20px;
font-size: 14px;
background: rgba(255,255,255,0.2);
color: white;
border: 2px solid rgba(255,255,255,0.5);
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
}
.secondary-btn:hover {
background: rgba(255,255,255,0.35);
border-color: white;
}
.danger-btn {
padding: 8px 14px;
font-size: 13px;
background: #e53935;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
}
.danger-btn:hover {
background: #c62828;
}
.result-section {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-top: 20px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: all 0.3s ease;
display: none;
}
.result-section.show {
display: block;
}
.result-section:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.section-header {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
color: white;
padding: 15px 20px;
}
.section-header h3 {
margin: 0;
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.section-content {
padding: 20px;
background: #fafafa;
}
.data-row {
display: flex;
margin: 12px 0;
padding: 10px;
background: white;
border-radius: 4px;
border-left: 3px solid #4CAF50;
align-items: center;
}
.data-label {
font-weight: bold;
color: #555;
min-width: 220px;
flex-shrink: 0;
font-size: 14px;
}
.data-value {
color: #333;
font-family: 'Courier New', monospace;
word-break: break-word;
font-size: 18px;
}
.data-value.highlight {
color: #4CAF50;
font-weight: bold;
font-size: 22px;
}
.data-value.warning {
color: #FF6B6B;
font-weight: bold;
font-size: 22px;
}
.help-text {
margin-top: 15px;
font-size: 14px;
color: rgba(255,255,255,0.9);
background: rgba(0,0,0,0.1);
padding: 10px 15px;
border-radius: 4px;
}
.help-text strong {
color: white;
}
/* Member list table */
.member-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
font-size: 14px;
}
.member-table th {
background: #f0f0f0;
padding: 10px 12px;
text-align: left;
font-weight: 600;
color: #444;
border-bottom: 2px solid #ddd;
}
.member-table td {
padding: 10px 12px;
border-bottom: 1px solid #eee;
vertical-align: middle;
}
.member-table tr:last-child td {
border-bottom: none;
}
.member-table tr:hover td {
background: #f9f9f9;
}
/* Status badge */
.status-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.up {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #a5d6a7;
}
.status-badge.down {
background: #ffebee;
color: #c62828;
border: 1px solid #ef9a9a;
}
/* Balance score ring */
.balance-score-container {
display: flex;
align-items: center;
gap: 20px;
padding: 15px;
background: white;
border-radius: 8px;
border: 1px solid #e0e0e0;
margin-bottom: 15px;
}
.balance-score-ring {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: bold;
color: white;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.balance-score-ring.excellent {
background: linear-gradient(135deg, #43a047, #1b5e20);
}
.balance-score-ring.good {
background: linear-gradient(135deg, #7cb342, #558b2f);
}
.balance-score-ring.fair {
background: linear-gradient(135deg, #fb8c00, #e65100);
}
.balance-score-ring.poor {
background: linear-gradient(135deg, #e53935, #b71c1c);
}
.balance-score-info {
flex: 1;
}
.balance-score-info h4 {
margin: 0 0 5px 0;
font-size: 18px;
color: #333;
}
.balance-score-info p {
margin: 0;
font-size: 13px;
color: #666;
line-height: 1.5;
}
/* Metrics grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.metric-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
text-align: center;
border-top: 3px solid #4CAF50;
}
.metric-card .metric-value {
font-size: 22px;
font-weight: bold;
color: #333;
font-family: 'Courier New', monospace;
}
.metric-card .metric-label {
font-size: 12px;
color: #888;
margin-top: 4px;
}
/* Chart container */
.chart-wrapper {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.chart-wrapper h4 {
margin: 0 0 15px 0;
color: #444;
font-size: 15px;
font-weight: 600;
}
/* Algorithm explanation */
.algo-explanation {
background: #fff8e1;
border: 1px solid #ffe082;
border-radius: 6px;
padding: 12px 15px;
font-size: 13px;
color: #5d4037;
line-height: 1.6;
margin-top: 10px;
}
.algo-explanation strong {
color: #3e2723;
}
/* Hash ring visualization */
.ring-wrapper {
display: flex;
align-items: flex-start;
gap: 20px;
}
#hashRingCanvas {
border: 1px solid #e0e0e0;
border-radius: 50%;
display: block;
flex-shrink: 0;
}
.ring-legend {
flex: 1;
}
.ring-legend-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
font-size: 13px;
border-bottom: 1px solid #f0f0f0;
}
.ring-legend-dot {
width: 14px;
height: 14px;
border-radius: 50%;
flex-shrink: 0;
}
/* Redistribution info */
.redistribution-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: white;
border-radius: 4px;
border-left: 3px solid #FF6B6B;
margin-bottom: 6px;
font-size: 14px;
}
.redistribution-row .arrow {
color: #FF6B6B;
font-size: 18px;
flex-shrink: 0;
}
.redistribution-row .move-label {
color: #555;
flex: 1;
}
.redistribution-row .move-count {
font-family: 'Courier New', monospace;
font-weight: bold;
color: #333;
}
/* Slider styling */
.slider-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 5px;
}
.slider-group label {
font-weight: 600;
font-size: 14px;
color: white;
display: flex;
justify-content: space-between;
}
.slider-group label span {
font-family: 'Courier New', monospace;
background: rgba(0,0,0,0.2);
padding: 2px 8px;
border-radius: 4px;
font-size: 13px;
}
input[type="range"] {
width: 100%;
accent-color: #4CAF50;
height: 6px;
cursor: pointer;
}
/* Controls area inside header */
.header-controls {
margin-top: 20px;
}
.control-group-label {
font-weight: 600;
font-size: 13px;
color: rgba(255,255,255,0.8);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
margin-top: 18px;
}
.add-member-row {
display: flex;
gap: 8px;
align-items: flex-end;
flex-wrap: wrap;
}
.add-member-row .form-group {
flex: 1;
min-width: 120px;
}
/* Failure simulation section */
.failure-section {
background: rgba(0,0,0,0.1);
border-radius: 8px;
padding: 15px;
margin-top: 18px;
}
.failure-section .control-group-label {
margin-top: 0;
}
.failure-row {
display: flex;
gap: 10px;
align-items: flex-end;
flex-wrap: wrap;
}
.failure-row .form-group {
flex: 1;
min-width: 140px;
}
.failure-btn-group {
display: flex;
gap: 8px;
}
/* Responsive design */
@media (max-width: 768px) {
.form-grid,
.form-grid-3 {
grid-template-columns: 1fr;
}
.metrics-grid {
grid-template-columns: 1fr 1fr;
}
.data-row {
flex-direction: column;
align-items: flex-start;
}
.data-label {
margin-bottom: 4px;
min-width: auto;
}
.ring-wrapper {
flex-direction: column;
}
#hashRingCanvas {
width: 220px !important;
height: 220px !important;
align-self: center;
}
.balance-score-container {
flex-direction: column;
text-align: center;
}
.add-member-row {
flex-direction: column;
}
.add-member-row .form-group {
min-width: auto;
width: 100%;
}
.failure-row {
flex-direction: column;
}
}
@media (max-width: 480px) {
.calc-header h2 {
font-size: 22px;
}
.metrics-grid {
grid-template-columns: 1fr;
}
}
Chart.js CDN
========== OVERSKRIFT/KONTROLLER ===========
========== MEDLEMSTABEL ===========
| ID |
IP:Port |
Vægt |
Status |
Handlinger |
befolket af JS
========== DISTRIBUTIONSRESULTATER ===========
Balancescore + algoritmeforklaring
Metrik gitter
Søjlediagram
Sessionsfordeling pr. medlem
Medlemsdetaljerækker
========== FEJL-OMDISTRIBUTION ==========
========== VISUALISERING AF HASHRING ==========
Læser ringen:Hvert medlem er placeret på flere punkter rundt om ringen
(virtuelle noder). En klient-IP hashes til en position på ringen, hvorefter anmodningen sendes
til nærmeste medlem med uret. Tilføjelse eller fjernelse af et medlem påvirker kun buen af
ring det ejede - minimere sessionsforstyrrelser.
========== HJÆLP / KONCEPTER ===========
Round Robin
Sessioner tildeles sekventielt: medlem 1, 2, 3, ..., N, 1, 2, ... Hvert medlem
modtager præcis 1/N af alle anmodninger. Ignorerer serverkapacitet og klienttilhørsforhold.
Enkel og forudsigelig, men fejler hårdt på heterogen hardware.
Kilde IP Hash
En hash af klientens IP modulo medlemsantallet vælger backend. Samme klient
når altid det samme medlem - nyttigt til stateful applikationer. Tilføjelse eller fjernelse
et medlem blander sigalleklientkortlægninger (N ændres til (N-1)/N af alle sessioner).
Vægtet
Hvert medlem modtager en andel, der er proportional med dens vægt i forhold til den samlede vægt
pool. Et medlem med vægt=4 får 4 gange så mange sessioner som et vægt=1 medlem. Brugt til at modellere
heterogen backend-kapacitet (f.eks. en VM vs en bare-metal-server).
Konsekvent hashing
Medlemmer og klienter er begge kortlagt på en cirkulær ring via hashing. Hver klient går
til nærmeste medlem med uret på ringen. Virtuelle noder (replikaer pr. medlem)
forbedre ensartet fordeling. Når et medlem fejler, flytter kun dets sessioner til
næste medlem på ringen — 1/N sessioner forstyrret vs. 100 % for modulo hashing.