Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"Bash(git add *)",
"Bash(echo \"EXIT:$?\")",
"Bash(echo \"BUILD_EXIT:$?\")",
"Bash(python -c \"import yaml; yaml.safe_load\\(open\\('.github/workflows/ci.yml'\\)\\)\")"
"Bash(python -c \"import yaml; yaml.safe_load\\(open\\('.github/workflows/ci.yml'\\)\\)\")",
"Bash(Get-ChildItem -Recurse \"C:\\\\Users\\\\pasichdev\\\\AndroidStudioProjects\\\\linqora\\\\design_handoff_linqora\")",
"Bash(Select-Object FullName, Length)"
]
}
}
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
sudo apt-get update -q
sudo apt-get install -y gcc libgl1-mesa-dev libx11-dev \
libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev \
gcc-mingw-w64-x86-64
libxxf86vm-dev gcc-mingw-w64-x86-64

- name: Set up Go
uses: actions/setup-go@v5
Expand Down
5 changes: 2 additions & 3 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ builds:
goarch:
- amd64
archives:
- format: tar.gz
# use zip for windows archives
- formats: [tar.gz]
format_overrides:
- goos: windows
format: zip
formats: [zip]
checksum:
name_template: 'checksums.txt'
snapshot:
Expand Down
2 changes: 1 addition & 1 deletion LinqoraHost/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module LinqoraHost

go 1.25.0
go 1.24

require (
fyne.io/fyne/v2 v2.7.3
Expand Down
22 changes: 13 additions & 9 deletions LinqoraHost/internal/collectors/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

// SystemMetrics represents a snapshot of the system's performance metrics.
type SystemMetrics struct {
CPUUMetrics metrics.CPUMetrics `json:"cpuMetrics"`
RamMetrics metrics.RamMetrics `json:"ramMetrics"`
Timestamp int64 `json:"timestamp"`
CPUUMetrics metrics.CPUMetrics `json:"cpuMetrics"`
RamMetrics metrics.RamMetrics `json:"ramMetrics"`
GpuLoadPercent int `json:"gpuLoadPercent"`
Timestamp int64 `json:"timestamp"`
}

// MetricsCollector periodically gathers and broadcasts system performance data.
Expand Down Expand Up @@ -113,7 +114,7 @@ func (mc *MetricsCollector) collectAndSend() {
mc.broadcaster(metricsJSON)
}

// collectMetrics retrieves CPU and RAM performance data.
// collectMetrics retrieves CPU, RAM, and GPU performance data.
func (mc *MetricsCollector) collectMetrics() (*SystemMetrics, error) {
cpuMetrics, err := metrics.GetCPUMetrics()
if err != nil {
Expand All @@ -125,11 +126,14 @@ func (mc *MetricsCollector) collectMetrics() (*SystemMetrics, error) {
return nil, err
}

metrics := &SystemMetrics{
CPUUMetrics: cpuMetrics,
RamMetrics: ramMetrics,
Timestamp: time.Now().Unix(),
gpuLoad := metrics.GetGPULoadPercent()

result := &SystemMetrics{
CPUUMetrics: cpuMetrics,
RamMetrics: ramMetrics,
GpuLoadPercent: gpuLoad,
Timestamp: time.Now().Unix(),
}

return metrics, nil
return result, nil
}
21 changes: 21 additions & 0 deletions LinqoraHost/internal/metrics/gpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,27 @@ func getMacOSGPUInfo() (GPUInfo, error) {
return info, nil
}

// ── GPU load ─────────────────────────────────────────────────────────────────

// GetGPULoadPercent returns the current GPU utilisation as an integer percentage
// (0-100). It tries nvidia-smi first; if that is unavailable or fails it
// returns 0 without an error so callers can treat missing data gracefully.
func GetGPULoadPercent() int {
out, err := exec.Command(
"nvidia-smi",
"--query-gpu=utilization.gpu",
"--format=csv,noheader,nounits",
).Output()
if err != nil || len(out) == 0 {
return 0
}
val, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return 0
}
return val
}

// ── helpers ──────────────────────────────────────────────────────────────────

func extractGPUModel(line string) string {
Expand Down
1 change: 1 addition & 0 deletions LinqoraHost/internal/power/power.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
Shutdown Action = iota
Restart
Lock
Sleep
)

// ExecutePowerAction performs a power management action.
Expand Down
2 changes: 2 additions & 0 deletions LinqoraHost/internal/power/power_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ func executePlatformAction(action Action) error {
case Lock:
cmd = exec.Command("osascript", "-e",
"tell application \"System Events\" to keystroke \"q\" using {command down, control down}")
case Sleep:
cmd = exec.Command("pmset", "sleepnow")
default:
return fmt.Errorf("unknown power action: %d", action)
}
Expand Down
2 changes: 2 additions & 0 deletions LinqoraHost/internal/power/power_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func executePlatformAction(action Action) error {
} else {
return fmt.Errorf("no suitable screen lock command found")
}
case Sleep:
cmd = exec.Command("systemctl", "suspend")
default:
return fmt.Errorf("unknown power action: %d", action)
}
Expand Down
4 changes: 4 additions & 0 deletions LinqoraHost/internal/power/power_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ func executePlatformAction(action Action) error {
return cmd.Run()
case Lock:
return lockWorkStationAPI()
case Sleep:
cmd := exec.Command("rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0")
log.Printf("Executing Windows sleep: %v", cmd.Args)
return cmd.Run()
default:
return fmt.Errorf("unknown power action: %d", action)
}
Expand Down
31 changes: 23 additions & 8 deletions LinqoraHost/internal/ws/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"net/http"
"runtime"
"sync"
"time"

Expand All @@ -24,6 +25,7 @@ import (
"LinqoraHost/internal/interfaces"

"github.com/gorilla/websocket"
gopshost "github.com/shirou/gopsutil/v4/host"
)

// WSServer represents the primary WebSocket server coordinating communication
Expand Down Expand Up @@ -364,16 +366,29 @@ func (s *WSServer) handleHostInfoMessage(client *Client) {
gpuInfo, _ := metrics.GetGPUInfo()
diskInfo, _ := metrics.GetDiskInfo()
batteryInfo, _ := metrics.GetBatteryInfo()
hostStat, _ := gopshost.Info()
uptime, _ := gopshost.Uptime()

kernelVersion := ""
platformVersion := ""
if hostStat != nil {
kernelVersion = hostStat.KernelVersion
platformVersion = hostStat.PlatformVersion
}

hostInfo := map[string]interface{}{
"os": deviceInfo.OS,
"hostname": deviceInfo.Hostname,
"su": privileges.CheckAdminPrivileges(),
"cpu": cpuInfo,
"ram": ramInfo,
"gpu": gpuInfo,
"disks": diskInfo,
"battery": batteryInfo,
"os": deviceInfo.OS,
"hostname": deviceInfo.Hostname,
"su": privileges.CheckAdminPrivileges(),
"cpu": cpuInfo,
"ram": ramInfo,
"gpu": gpuInfo,
"disks": diskInfo,
"battery": batteryInfo,
"uptime": uptime,
"architecture": runtime.GOARCH,
"kernelVersion": kernelVersion,
"platformVersion": platformVersion,
}

client.SendSuccess("host_info", hostInfo)
Expand Down
36 changes: 36 additions & 0 deletions linqoraremote/lib/core/themes/lx_theme.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';

// Design tokens — Linqora Stylish Minimalism palette
const Color lxBg = Color(0xFF0A0C10);
const Color lxBgGrad = Color(0xFF1A1F2B);
const Color lxSurface = Color(0xFF0F172A);
const Color lxAccent = Color(0xFF00E5FF);
const Color lxText = Color(0xFFFFFFFF);
const Color lxTextDim = Color(0x99FFFFFF); // 0.6
const Color lxTextFaint = Color(0x59FFFFFF); // 0.35
const Color lxTextGhost = Color(0x2EFFFFFF); // 0.18
const Color lxHairline = Color(0x14FFFFFF); // 0.08
const Color lxHairlineHi= Color(0x24FFFFFF); // 0.14
const Color lxGlass = Color(0x0AFFFFFF); // 0.04
const Color lxGlass2 = Color(0x0FFFFFFF); // 0.06
const Color lxRed = Color(0xFFFF4D5E);
const Color lxAmber = Color(0xFFFFB547);
const Color lxGreen = Color(0xFF3BD2A2);

// Radius tokens
const double lxRadiusCard = 18;
const double lxRadiusTile = 12;
const double lxRadiusInner = 10;
const double lxRadiusModal = 22;
const double lxRadiusTabBar = 22;

// Spacing scale (px)
const double sp4 = 4;
const double sp6 = 6;
const double sp8 = 8;
const double sp10 = 10;
const double sp12 = 12;
const double sp14 = 14;
const double sp18 = 18;
const double sp20 = 20;
const double sp22 = 22;
38 changes: 38 additions & 0 deletions linqoraremote/lib/data/models/host_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,52 @@ class DiskInfo {
}
}

class BatteryInfo {
final bool isPresent;
final int level;
final bool isCharging;
final String status;

const BatteryInfo({
this.isPresent = false,
this.level = 0,
this.isCharging = false,
this.status = 'Unknown',
});

factory BatteryInfo.fromJson(Map<String, dynamic> json) {
return BatteryInfo(
isPresent: json['isPresent'] ?? false,
level: (json['level'] ?? 0).toInt(),
isCharging: json['isCharging'] ?? false,
status: json['status'] ?? 'Unknown',
);
}
}

class HostSystemInfo {
final BaseSystemInfo baseInfo;
final CPUInfo cpu;
final RAMInfo ram;
final GPUInfo gpu;
final List<DiskInfo> disks;
final BatteryInfo battery;
final int uptime;
final String architecture;
final String kernelVersion;
final String platformVersion;

HostSystemInfo({
required this.baseInfo,
required this.cpu,
required this.ram,
required this.gpu,
this.disks = const [],
this.battery = const BatteryInfo(),
this.uptime = 0,
this.architecture = '',
this.kernelVersion = '',
this.platformVersion = '',
});

factory HostSystemInfo.fromJson(Map<String, dynamic> json) {
Expand All @@ -150,6 +183,11 @@ class HostSystemInfo {
ram: RAMInfo.fromJson(json['ram'] ?? {}),
gpu: GPUInfo.fromJson(json['gpu'] ?? {}),
disks: disks,
uptime: (json['uptime'] ?? 0).toInt(),
architecture: json['architecture'] ?? '',
kernelVersion: json['kernelVersion'] ?? '',
platformVersion: json['platformVersion'] ?? '',
battery: json['battery'] != null ? BatteryInfo.fromJson(json['battery']) : const BatteryInfo(),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,45 @@ class MonitoringController extends GetxController {
super.onClose();
}

// Buffer for incoming metrics to provide smooth, delayed updates.
final List<({DateTime timestamp, Map<String, dynamic> data})> _metricsBuffer = [];

void _handleMetricsUpdate(Map<String, dynamic> data) {
final rawData = data['data'];
if (rawData == null || rawData is! Map<String, dynamic>) {
AppLogger.release(
'Invalid metrics payload — missing or malformed "data" field',
module: 'MonitoringController',
);
return;
if (rawData == null || rawData is! Map<String, dynamic>) return;

// Add to buffer with current timestamp
_metricsBuffer.add((timestamp: DateTime.now(), data: rawData));

// Start processing timer if not already running
_startBufferTimer();
}

bool _isTimerRunning = false;
void _startBufferTimer() {
if (_isTimerRunning) return;
_isTimerRunning = true;

// Check buffer every 500ms
Stream.periodic(const Duration(milliseconds: 500)).listen((_) {
_processBuffer();
});
}

void _processBuffer() {
if (_metricsBuffer.isEmpty) return;

final now = DateTime.now();
final delay = const Duration(seconds: 3);

// Process all metrics that have reached the delay threshold
while (_metricsBuffer.isNotEmpty && now.difference(_metricsBuffer.first.timestamp) >= delay) {
final entry = _metricsBuffer.removeAt(0);
_applyMetrics(entry.data);
}
}

void _applyMetrics(Map<String, dynamic> rawData) {
try {
final cpuJson = rawData['cpuMetrics'];
final ramJson = rawData['ramMetrics'];
Expand All @@ -120,7 +149,7 @@ class MonitoringController extends GetxController {
);
} catch (e) {
AppLogger.release(
'Error parsing metrics: $e',
'Error applying metrics: $e',
module: 'MonitoringController',
);
}
Expand All @@ -129,6 +158,7 @@ class MonitoringController extends GetxController {
void _resetMetrics() {
currentCPUMetrics.value = null;
currentRAMMetrics.value = null;
_metricsBuffer.clear();
_tempBuf.clear();
_cpuBuf.clear();
_ramBuf.clear();
Expand Down
Loading
Loading