This commit is contained in:
2026-01-10 16:04:38 +01:00
parent f172678560
commit 0ce18d87bc
13 changed files with 779 additions and 226 deletions

View File

@@ -117,11 +117,8 @@
<div class="row">
<AntennaGenius status={status?.antenna_genius} />
<RotatorGenius status={status?.rotator_genius} />
</div>
<div class="row">
<Ultrabeam status={status?.ultrabeam} />
<RotatorGenius status={status?.rotator_genius} />
</div>
</div>
</main>
@@ -181,13 +178,41 @@
}
.solar-item {
color: rgba(255, 255, 255, 0.8);
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
}
.solar-item .value {
color: var(--accent-teal);
font-weight: 500;
font-weight: 700;
margin-left: 4px;
font-size: 14px;
}
.solar-item:nth-child(1) .value { /* SFI */
color: #ffa726;
text-shadow: 0 0 8px rgba(255, 167, 38, 0.5);
}
.solar-item:nth-child(2) .value { /* Spots */
color: #66bb6a;
text-shadow: 0 0 8px rgba(102, 187, 106, 0.5);
}
.solar-item:nth-child(3) .value { /* A */
color: #42a5f5;
text-shadow: 0 0 8px rgba(66, 165, 245, 0.5);
}
.solar-item:nth-child(4) .value { /* K */
color: #ef5350;
text-shadow: 0 0 8px rgba(239, 83, 80, 0.5);
}
.solar-item:nth-child(5) .value { /* G */
color: #ab47bc;
text-shadow: 0 0 8px rgba(171, 71, 188, 0.5);
}
.header-right {

View File

@@ -62,40 +62,26 @@
</div>
<div class="metrics">
<!-- Power Display - Big and Bold -->
<div class="power-display">
<div class="power-main">
<div class="power-value">{powerForward.toFixed(0)}<span class="unit">W</span></div>
<div class="power-label">Forward Power</div>
</div>
<div class="power-bar">
<div class="power-bar-fill" style="width: {powerPercent}%">
<div class="power-bar-glow"></div>
<!-- Power Display + SWR Side by Side -->
<div class="power-swr-row">
<div class="power-section">
<div class="power-header">
<span class="power-label-inline">Power</span>
<span class="power-value-inline">{powerForward.toFixed(0)} W</span>
</div>
<div class="power-scale">
<span>0</span>
<span>1000</span>
<span>2000</span>
<div class="power-bar-container">
<div class="power-bar-bg">
<div class="power-bar-fill" style="width: {powerPercent}%">
<div class="power-bar-glow"></div>
</div>
</div>
</div>
</div>
</div>
<!-- SWR Circle Indicator -->
<div class="swr-container">
<div class="swr-circle" style="--swr-color: {swrColor}">
<div class="swr-value">{swr.toFixed(2)}</div>
<div class="swr-label">SWR</div>
</div>
<div class="swr-status">
{#if swr < 1.5}
<span class="status-text good">Excellent</span>
{:else if swr < 2.0}
<span class="status-text ok">Good</span>
{:else if swr < 3.0}
<span class="status-text warning">Caution</span>
{:else}
<span class="status-text danger">High!</span>
{/if}
<!-- SWR Circle Compact -->
<div class="swr-circle-compact" style="--swr-color: {swrColor}">
<div class="swr-value-compact">{swr.toFixed(2)}</div>
<div class="swr-label-compact">SWR</div>
</div>
</div>
@@ -147,8 +133,8 @@
<!-- Fan Control -->
<div class="fan-control">
<label class="control-label">Fan Mode</label>
<select value={fanMode} on:change={(e) => setFanMode(e.target.value)}>
<label for="fan-mode-select" class="control-label">Fan Mode</label>
<select id="fan-mode-select" value={fanMode} on:change={(e) => setFanMode(e.target.value)}>
<option value="STANDARD">Standard</option>
<option value="CONTEST">Contest</option>
<option value="BROADCAST">Broadcast</option>
@@ -235,10 +221,108 @@
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
gap: 10px;
}
/* Power Display */
/* Power + SWR Row */
.power-swr-row {
display: flex;
gap: 16px;
align-items: center;
}
.power-section {
flex: 1;
background: rgba(15, 23, 42, 0.6);
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.power-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.power-label-inline {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.power-value-inline {
font-size: 22px;
font-weight: 600;
color: #66bb6a;
}
.power-bar-container {
position: relative;
}
.power-bar-bg {
width: 100%;
height: 28px;
background: rgba(0, 0, 0, 0.3);
border-radius: 14px;
overflow: hidden;
position: relative;
}
.power-bar-fill {
position: relative;
height: 100%;
background: linear-gradient(90deg, #4caf50, #ffc107, #ff9800, #f44336);
border-radius: 14px;
transition: width 0.3s ease;
}
.power-bar-glow {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5));
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.swr-circle-compact {
width: 90px;
height: 90px;
border-radius: 50%;
background: radial-gradient(circle, rgba(79, 195, 247, 0.1), transparent);
border: 4px solid var(--swr-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 0 25px var(--swr-color);
flex-shrink: 0;
}
.swr-value-compact {
font-size: 28px;
font-weight: 700;
color: var(--swr-color);
}
.swr-label-compact {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
margin-top: 2px;
}
.power-display {
display: flex;
flex-direction: column;
@@ -258,7 +342,7 @@
}
.power-value .unit {
font-size: 24px;
font-size: 20px;
color: var(--text-secondary);
margin-left: 4px;
}
@@ -314,7 +398,7 @@
.swr-container {
display: flex;
align-items: center;
gap: 16px;
gap: 10px;
}
.swr-circle {
@@ -331,7 +415,7 @@
}
.swr-value {
font-size: 24px;
font-size: 20px;
font-weight: 300;
color: var(--swr-color);
}
@@ -422,7 +506,7 @@
}
.param-value {
font-size: 18px;
font-size: 16px;
font-weight: 300;
color: var(--text-primary);
margin-top: 2px;

View File

@@ -53,6 +53,29 @@
console.error('Failed to stop:', err);
}
}
// Handle click on compass to set heading
function handleCompassClick(event) {
const svg = event.currentTarget;
const rect = svg.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;
// Get click position relative to center
const x = event.clientX - rect.left - centerX;
const y = event.clientY - rect.top - centerY;
// Calculate angle (0° = North/top, clockwise)
let angle = Math.atan2(x, -y) * (180 / Math.PI);
if (angle < 0) angle += 360;
// Round to nearest 5 degrees
const roundedHeading = Math.round(angle / 5) * 5;
// Set target and go
targetHeading = roundedHeading;
goToHeading();
}
</script>
<div class="card">
@@ -62,15 +85,29 @@
</div>
<div class="metrics">
<!-- Current Heading Display -->
<div class="heading-display">
<div class="heading-label">CURRENT HEADING</div>
<div class="heading-value">{heading}°</div>
<!-- Current Heading Display with Compact Controls -->
<div class="heading-controls-row">
<div class="heading-display-compact">
<div class="heading-label">CURRENT HEADING</div>
<div class="heading-value">{heading}°</div>
</div>
<div class="controls-compact">
<button class="btn-mini ccw" on:click={rotateCCW} title="Rotate Counter-Clockwise">
</button>
<button class="btn-mini stop" on:click={stop} title="Stop Rotation">
</button>
<button class="btn-mini cw" on:click={rotateCW} title="Rotate Clockwise">
</button>
</div>
</div>
<!-- Map with Beam -->
<div class="map-container">
<svg viewBox="0 0 300 300" class="map-svg">
<svg viewBox="0 0 300 300" class="map-svg clickable-compass" on:click={handleCompassClick}>
<defs>
<!-- Gradient for beam -->
<radialGradient id="beamGradient">
@@ -139,32 +176,6 @@
</div>
<!-- Go To Heading -->
<div class="goto-container">
<input
type="number"
min="0"
max="359"
bind:value={targetHeading}
placeholder="Enter heading"
class="heading-input"
/>
<button class="go-btn" on:click={goToHeading}>GO</button>
</div>
<!-- Control Buttons -->
<div class="controls">
<button class="control-btn ccw" on:click={rotateCCW}>
<span class="arrow"></span>
CCW
</button>
<button class="control-btn stop" on:click={stop}>
STOP
</button>
<button class="control-btn cw" on:click={rotateCW}>
<span class="arrow"></span>
CW
</button>
</div>
</div>
</div>
@@ -212,7 +223,7 @@
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
gap: 10px;
}
/* Heading Display */
@@ -223,6 +234,71 @@
border-radius: 6px;
border: 1px solid rgba(79, 195, 247, 0.3);
}
.heading-controls-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 8px;
background: rgba(79, 195, 247, 0.1);
border-radius: 6px;
border: 1px solid rgba(79, 195, 247, 0.3);
}
.heading-display-compact {
flex: 1;
text-align: center;
}
.controls-compact {
display: flex;
gap: 6px;
}
.btn-mini {
width: 36px;
height: 36px;
border: none;
border-radius: 6px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-mini.ccw {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-mini.ccw:hover {
transform: rotate(-15deg) scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-mini.stop {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-mini.stop:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4);
}
.btn-mini.cw {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.btn-mini.cw:hover {
transform: rotate(15deg) scale(1.05);
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4);
}
.heading-label {
font-size: 10px;
@@ -253,10 +329,19 @@
max-width: 300px;
height: auto;
}
.clickable-compass {
cursor: crosshair;
user-select: none;
}
.clickable-compass:hover {
filter: brightness(1.1);
}
.cardinal {
fill: var(--accent-cyan);
font-size: 18px;
font-size: 16px;
font-weight: 700;
text-shadow: 0 0 10px rgba(79, 195, 247, 0.8);
}
@@ -353,4 +438,4 @@
font-size: 20px;
line-height: 1;
}
</style>
</style>

View File

@@ -57,40 +57,26 @@
</div>
<div class="metrics">
<!-- Power Display -->
<div class="power-display">
<div class="power-main">
<div class="power-value">{powerForward.toFixed(0)}<span class="unit">W</span></div>
<div class="power-label">Forward Power</div>
</div>
<div class="power-bar">
<div class="power-bar-fill" style="width: {powerPercent}%">
<div class="power-bar-glow"></div>
<!-- Power Display + SWR Side by Side -->
<div class="power-swr-row">
<div class="power-section">
<div class="power-header">
<span class="power-label-inline">Power</span>
<span class="power-value-inline">{powerForward.toFixed(0)} W</span>
</div>
<div class="power-scale">
<span>0</span>
<span>1000</span>
<span>2000</span>
<div class="power-bar-container">
<div class="power-bar-bg">
<div class="power-bar-fill" style="width: {powerPercent}%">
<div class="power-bar-glow"></div>
</div>
</div>
</div>
</div>
</div>
<!-- SWR Circle -->
<div class="swr-container">
<div class="swr-circle" style="--swr-color: {swrColor}">
<div class="swr-value">{swr.toFixed(2)}</div>
<div class="swr-label">SWR</div>
</div>
<div class="swr-status">
{#if swr < 1.5}
<span class="status-text good">Excellent</span>
{:else if swr < 2.0}
<span class="status-text ok">Good</span>
{:else if swr < 3.0}
<span class="status-text warning">Caution</span>
{:else}
<span class="status-text danger">High!</span>
{/if}
<!-- SWR Circle Compact -->
<div class="swr-circle-compact" style="--swr-color: {swrColor}">
<div class="swr-value-compact">{swr.toFixed(2)}</div>
<div class="swr-label-compact">SWR</div>
</div>
</div>
@@ -219,55 +205,63 @@
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
gap: 10px;
}
/* Power Display */
.power-display {
/* Power + SWR Row */
.power-swr-row {
display: flex;
flex-direction: column;
gap: 8px;
gap: 16px;
align-items: center;
}
.power-main {
text-align: center;
.power-section {
flex: 1;
background: rgba(15, 23, 42, 0.6);
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.power-value {
font-size: 48px;
font-weight: 200;
color: var(--accent-cyan);
line-height: 1;
text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
.power-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.power-value .unit {
font-size: 24px;
color: var(--text-secondary);
margin-left: 4px;
}
.power-label {
font-size: 11px;
color: var(--text-muted);
.power-label-inline {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
text-transform: uppercase;
letter-spacing: 1px;
margin-top: 4px;
letter-spacing: 0.5px;
}
.power-bar {
.power-value-inline {
font-size: 22px;
font-weight: 600;
color: #66bb6a;
}
.power-bar-container {
position: relative;
height: 8px;
background: var(--bg-tertiary);
border-radius: 4px;
}
.power-bar-bg {
width: 100%;
height: 28px;
background: rgba(0, 0, 0, 0.3);
border-radius: 14px;
overflow: hidden;
position: relative;
}
.power-bar-fill {
position: relative;
height: 100%;
background: linear-gradient(90deg, #4caf50, #ffc107, #ff9800, #f44336);
border-radius: 4px;
border-radius: 14px;
transition: width 0.3s ease;
}
@@ -286,19 +280,44 @@
100% { transform: translateX(100%); }
}
.power-scale {
.swr-circle-compact {
width: 90px;
height: 90px;
border-radius: 50%;
background: radial-gradient(circle, rgba(79, 195, 247, 0.1), transparent);
border: 4px solid var(--swr-color);
display: flex;
justify-content: space-between;
font-size: 9px;
color: var(--text-muted);
margin-top: 4px;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 0 25px var(--swr-color);
flex-shrink: 0;
}
.swr-value-compact {
font-size: 28px;
font-weight: 700;
color: var(--swr-color);
}
.swr-label-compact {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
text-transform: uppercase;
margin-top: 2px;
}
.power-display {
display: flex;
flex-direction: column;
gap: 8px;
}
/* SWR Circle */
.swr-container {
display: flex;
align-items: center;
gap: 16px;
gap: 10px;
}
.swr-circle {
@@ -315,7 +334,7 @@
}
.swr-value {
font-size: 24px;
font-size: 20px;
font-weight: 300;
color: var(--swr-color);
}
@@ -361,7 +380,7 @@
}
.cap-value {
font-size: 32px;
font-size: 20px;
font-weight: 300;
color: var(--accent-cyan);
text-shadow: 0 0 15px rgba(79, 195, 247, 0.5);
@@ -472,6 +491,6 @@
}
.tune-icon {
font-size: 18px;
font-size: 16px;
}
</style>

View File

@@ -13,34 +13,96 @@
$: elementLengths = status?.element_lengths || [];
$: firmwareVersion = status ? `${status.firmware_major}.${status.firmware_minor}` : '0.0';
// Band names mapping
// Band names mapping - VL2.3 covers 6M to 40M only
// Band 0=6M, 1=10M, 2=12M, 3=15M, 4=17M, 5=20M, 6=30M, 7=40M
const bandNames = [
'160M', '80M', '60M', '40M', '30M', '20M',
'17M', '15M', '12M', '10M', '6M'
'6M', '10M', '12M', '15M', '17M', '20M', '30M', '40M'
];
// Detect band from frequency
$: detectedBand = detectBandFromFrequency(frequency, band);
function detectBandFromFrequency(freq, bandIndex) {
// If band index is valid (0-7), use it directly
if (bandIndex >= 0 && bandIndex <= 7) {
return bandNames[bandIndex];
}
// Otherwise detect from frequency (in kHz)
if (freq >= 7000 && freq <= 7300) return '40M';
if (freq >= 10100 && freq <= 10150) return '30M';
if (freq >= 14000 && freq <= 14350) return '20M';
if (freq >= 18068 && freq <= 18168) return '17M';
if (freq >= 21000 && freq <= 21450) return '15M';
if (freq >= 24890 && freq <= 24990) return '12M';
if (freq >= 28000 && freq <= 29700) return '10M';
if (freq >= 50000 && freq <= 54000) return '6M';
return 'Unknown';
}
// Direction names
const directionNames = ['Normal', '180°', 'Bi-Dir'];
// Auto-track threshold options
const thresholdOptions = [
{ value: 25, label: '25 kHz' },
{ value: 50, label: '50 kHz' },
{ value: 100, label: '100 kHz' }
];
// Auto-track state
let autoTrackEnabled = true; // Default enabled
let autoTrackThreshold = 25; // Default 25 kHz
// Form state
let targetFreq = 0;
let targetDirection = 0;
// Auto-update targetDirection when status changes
$: targetDirection = direction;
// Element names based on band (corrected order: 0=6M ... 10=160M)
$: elementNames = getElementNames(band);
function getElementNames(band) {
// 30M (band 6) and 40M (band 7): Reflector (inverted), Radiator (inverted)
if (band === 6 || band === 7) {
return ['Radiator (30/40M)', 'Reflector (30/40M)', null];
}
// 6M to 20M (bands 0-5): Reflector, Radiator, Director 1
if (band >= 0 && band <= 5) {
return ['Reflector', 'Radiator', 'Director 1'];
}
// Default
return ['Element 1', 'Element 2', 'Element 3'];
}
// Element calibration state
let calibrationMode = false;
let selectedElement = 0;
let elementAdjustment = 0;
async function setFrequency() {
if (targetFreq < 1800 || targetFreq > 30000) {
alert('Frequency must be between 1.8 MHz and 30 MHz');
return;
async function setDirection() {
if (frequency === 0) {
return; // Silently skip if no frequency
}
try {
await api.ultrabeam.setFrequency(targetFreq, targetDirection);
// Send command to antenna with current frequency and new direction
await api.ultrabeam.setFrequency(frequency, targetDirection);
// Also save direction preference for auto-track
await api.ultrabeam.setDirection(targetDirection);
} catch (err) {
console.error('Failed to set frequency:', err);
alert('Failed to set frequency');
// Log error but don't alert - code 30 (busy) is normal
console.log('Direction change sent (may show code 30 if busy):', err);
}
}
async function updateAutoTrack() {
try {
await api.ultrabeam.setAutoTrack(autoTrackEnabled, autoTrackThreshold);
} catch (err) {
console.error('Failed to update auto-track:', err);
alert('Failed to update auto-track settings');
}
}
@@ -88,50 +150,56 @@
<div class="status-item">
<div class="status-label">Band</div>
<div class="status-value band">{bandNames[band] || 'Unknown'}</div>
<div class="status-value band">{detectedBand}</div>
</div>
<div class="status-item">
<div class="status-label">Direction</div>
<div class="status-value direction">{directionNames[direction]}</div>
</div>
<div class="status-item">
<div class="status-label">Firmware</div>
<div class="status-value fw">v{firmwareVersion}</div>
</div>
</div>
<!-- Frequency Control -->
<div class="control-section">
<h3>Frequency Control</h3>
<div class="freq-control">
<div class="input-group">
<label for="target-freq">Target Frequency (KHz)</label>
<input
id="target-freq"
type="number"
bind:value={targetFreq}
min="1800"
max="30000"
step="1"
placeholder="e.g. 14200"
/>
</div>
<!-- Auto-Track Control -->
<div class="control-section compact">
<h3>Auto Tracking</h3>
<div class="auto-track-controls">
<label class="toggle-label">
<input type="checkbox" bind:checked={autoTrackEnabled} on:change={updateAutoTrack} />
<span>Enable Auto-Track from Tuner</span>
</label>
<div class="input-group">
<label for="target-dir">Direction</label>
<select id="target-dir" bind:value={targetDirection}>
<option value={0}>Normal</option>
<option value={1}>180°</option>
<option value={2}>Bi-Directional</option>
<div class="threshold-group">
<label for="threshold-select">Threshold:</label>
<select id="threshold-select" bind:value={autoTrackThreshold} on:change={updateAutoTrack}>
{#each thresholdOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<button class="btn-primary" on:click={setFrequency}>
<span class="icon">📡</span>
Set Frequency
</button>
<div class="direction-buttons">
<button
class="dir-btn normal"
class:active={targetDirection === 0}
on:click={() => { targetDirection = 0; setDirection(); }}
>
Normal
</button>
<button
class="dir-btn rotate180"
class:active={targetDirection === 1}
on:click={() => { targetDirection = 1; setDirection(); }}
>
180°
</button>
<button
class="dir-btn bidir"
class:active={targetDirection === 2}
on:click={() => { targetDirection = 2; setDirection(); }}
>
Bi-Dir
</button>
</div>
</div>
</div>
@@ -146,22 +214,25 @@
</div>
{/if}
<!-- Element Lengths Display -->
<!-- Element Lengths Display - HIDDEN: Command 9 returns status instead -->
<!--
<div class="elements-section">
<h3>Element Lengths (mm)</h3>
<div class="elements-grid">
{#each elementLengths as length, i}
{#if length > 0}
{#each elementLengths.slice(0, 3) as length, i}
{#if length > 0 && elementNames[i]}
<div class="element-item">
<div class="element-label">Element {i + 1}</div>
<div class="element-label">{elementNames[i]}</div>
<div class="element-value">{length} mm</div>
</div>
{/if}
{/each}
</div>
</div>
-->
<!-- Calibration Mode -->
<!-- Calibration Mode - HIDDEN: Command 9 doesn't work -->
<!--
<div class="calibration-section">
<div class="section-header">
<h3>Calibration</h3>
@@ -179,9 +250,9 @@
<div class="input-group">
<label for="element-select">Element</label>
<select id="element-select" bind:value={selectedElement}>
{#each elementLengths as length, i}
{#if length > 0}
<option value={i}>Element {i + 1} ({length}mm)</option>
{#each elementLengths.slice(0, 3) as length, i}
{#if length > 0 && elementNames[i]}
<option value={i}>{elementNames[i]} ({length}mm)</option>
{/if}
{/each}
</select>
@@ -209,6 +280,7 @@
</div>
{/if}
</div>
-->
<!-- Actions -->
<div class="actions">
@@ -224,7 +296,7 @@
.card {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%);
border-radius: 16px;
padding: 24px;
padding: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(79, 195, 247, 0.2);
}
@@ -240,7 +312,7 @@
h2 {
margin: 0;
font-size: 24px;
font-size: 20px;
font-weight: 600;
background: linear-gradient(135deg, #4fc3f7 0%, #03a9f4 100%);
-webkit-background-clip: text;
@@ -278,14 +350,14 @@
.metrics {
display: flex;
flex-direction: column;
gap: 24px;
gap: 12px;
}
/* Status Grid */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
gap: 10px;
}
.status-item {
@@ -304,31 +376,139 @@
}
.status-value {
font-size: 20px;
font-weight: 600;
font-size: 22px;
font-weight: 700;
color: #4fc3f7;
text-shadow: 0 0 10px rgba(79, 195, 247, 0.5);
}
.status-value.freq {
color: #66bb6a;
font-size: 24px;
font-size: 22px;
text-shadow: 0 0 10px rgba(102, 187, 106, 0.5);
}
.status-value.band {
color: #ffa726;
text-shadow: 0 0 10px rgba(255, 167, 38, 0.5);
}
.status-value.direction {
color: #ab47bc;
text-shadow: 0 0 10px rgba(171, 71, 188, 0.5);
}
/* Control Section */
.control-section {
background: rgba(15, 23, 42, 0.4);
padding: 20px;
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.control-section.compact {
padding: 16px;
}
.auto-track-controls {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.toggle-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: #fff;
font-size: 14px;
}
.toggle-label input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.threshold-group {
display: flex;
align-items: center;
gap: 8px;
}
.threshold-group label {
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
white-space: nowrap;
}
.direction-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 12px;
}
.dir-btn {
padding: 14px 20px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s;
color: white;
letter-spacing: 0.5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.dir-btn.normal {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.dir-btn.normal:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
}
.dir-btn.normal.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 0 25px rgba(102, 126, 234, 0.8), 0 6px 20px rgba(102, 126, 234, 0.5);
transform: translateY(-2px);
}
.dir-btn.rotate180 {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.dir-btn.rotate180:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(245, 87, 108, 0.5);
}
.dir-btn.rotate180.active {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
box-shadow: 0 0 25px rgba(245, 87, 108, 0.8), 0 6px 20px rgba(245, 87, 108, 0.5);
transform: translateY(-2px);
}
.dir-btn.bidir {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.dir-btn.bidir:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 172, 254, 0.5);
}
.dir-btn.bidir.active {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
box-shadow: 0 0 25px rgba(79, 172, 254, 0.8), 0 6px 20px rgba(79, 172, 254, 0.5);
transform: translateY(-2px);
}
.freq-control {
display: grid;
@@ -437,7 +617,7 @@
/* Progress */
.progress-section {
background: rgba(15, 23, 42, 0.4);
padding: 20px;
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(255, 193, 7, 0.3);
}
@@ -468,7 +648,7 @@
/* Elements */
.elements-section {
background: rgba(15, 23, 42, 0.4);
padding: 20px;
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
@@ -502,7 +682,7 @@
/* Calibration */
.calibration-section {
background: rgba(255, 152, 0, 0.05);
padding: 20px;
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(255, 152, 0, 0.3);
}
@@ -517,7 +697,7 @@
.calibration-controls {
display: flex;
flex-direction: column;
gap: 16px;
gap: 10px;
}
.warning-text {

View File

@@ -97,5 +97,9 @@ export const api = {
body: JSON.stringify({ frequency, direction }),
}),
retract: () => request('/ultrabeam/retract', { method: 'POST' }),
setAutoTrack: (enabled, threshold) => request('/ultrabeam/autotrack', {
method: 'POST',
body: JSON.stringify({ enabled, threshold }),
}),
},
};