feat: Add stdin console commands, SQLite persistence, and P2P enhancements
- Add stdin pipe support for sending console commands to running miners (XMRig/TT-Miner) - Add base64 encoding for log transport to preserve ANSI escape codes - Add SQLite database for persistent hashrate history storage - Enhance P2P worker to handle remote miner commands (start/stop/stats/logs) - Add console UI page with ANSI-to-HTML rendering and command input - Add E2E tests for navigation, UI elements, and miner start flow - Update Dockerfile to use Go 1.24 with GOTOOLCHAIN=auto 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BIN
.playwright-mcp/console-page.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
.playwright-mcp/console-with-colors.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
.playwright-mcp/console-with-logs.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
.playwright-mcp/dashboard-after-nav.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
.playwright-mcp/dashboard-after-wait.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
.playwright-mcp/dashboard-direct-nav.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
.playwright-mcp/dashboard-refresh-fixed.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
.playwright-mcp/dashboard-test.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
|
|
@ -92,6 +92,7 @@ type BaseMiner struct {
|
||||||
API *API `json:"api"`
|
API *API `json:"api"`
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
|
stdinPipe io.WriteCloser `json:"-"`
|
||||||
HashrateHistory []HashratePoint `json:"hashrateHistory"`
|
HashrateHistory []HashratePoint `json:"hashrateHistory"`
|
||||||
LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"`
|
LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"`
|
||||||
LastLowResAggregation time.Time `json:"-"`
|
LastLowResAggregation time.Time `json:"-"`
|
||||||
|
|
@ -135,9 +136,33 @@ func (b *BaseMiner) Stop() error {
|
||||||
return errors.New("miner is not running")
|
return errors.New("miner is not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close stdin pipe if open
|
||||||
|
if b.stdinPipe != nil {
|
||||||
|
b.stdinPipe.Close()
|
||||||
|
b.stdinPipe = nil
|
||||||
|
}
|
||||||
|
|
||||||
return b.cmd.Process.Kill()
|
return b.cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteStdin sends input to the miner's stdin (for console commands).
|
||||||
|
func (b *BaseMiner) WriteStdin(input string) error {
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
|
||||||
|
if !b.Running || b.stdinPipe == nil {
|
||||||
|
return errors.New("miner is not running or stdin not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append newline if not present
|
||||||
|
if !strings.HasSuffix(input, "\n") {
|
||||||
|
input += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := b.stdinPipe.Write([]byte(input))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Uninstall removes all files related to the miner.
|
// Uninstall removes all files related to the miner.
|
||||||
func (b *BaseMiner) Uninstall() error {
|
func (b *BaseMiner) Uninstall() error {
|
||||||
return os.RemoveAll(b.GetPath())
|
return os.RemoveAll(b.GetPath())
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ type Miner interface {
|
||||||
AddHashratePoint(point HashratePoint)
|
AddHashratePoint(point HashratePoint)
|
||||||
ReduceHashrateHistory(now time.Time)
|
ReduceHashrateHistory(now time.Time)
|
||||||
GetLogs() []string
|
GetLogs() []string
|
||||||
|
WriteStdin(input string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallationDetails contains information about an installed miner.
|
// InstallationDetails contains information about an installed miner.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package mining
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
@ -129,6 +130,7 @@ func (s *Service) SetupRoutes() {
|
||||||
minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats)
|
minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats)
|
||||||
minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory)
|
minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory)
|
||||||
minersGroup.GET("/:miner_name/logs", s.handleGetMinerLogs)
|
minersGroup.GET("/:miner_name/logs", s.handleGetMinerLogs)
|
||||||
|
minersGroup.POST("/:miner_name/stdin", s.handleMinerStdin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Historical data endpoints (database-backed)
|
// Historical data endpoints (database-backed)
|
||||||
|
|
@ -471,11 +473,11 @@ func (s *Service) handleGetMinerHashrateHistory(c *gin.Context) {
|
||||||
|
|
||||||
// handleGetMinerLogs godoc
|
// handleGetMinerLogs godoc
|
||||||
// @Summary Get miner log output
|
// @Summary Get miner log output
|
||||||
// @Description Get the captured stdout/stderr output from a running miner
|
// @Description Get the captured stdout/stderr output from a running miner. Log lines are base64 encoded to preserve ANSI escape codes and special characters.
|
||||||
// @Tags miners
|
// @Tags miners
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param miner_name path string true "Miner Name"
|
// @Param miner_name path string true "Miner Name"
|
||||||
// @Success 200 {array} string
|
// @Success 200 {array} string "Base64 encoded log lines"
|
||||||
// @Router /miners/{miner_name}/logs [get]
|
// @Router /miners/{miner_name}/logs [get]
|
||||||
func (s *Service) handleGetMinerLogs(c *gin.Context) {
|
func (s *Service) handleGetMinerLogs(c *gin.Context) {
|
||||||
minerName := c.Param("miner_name")
|
minerName := c.Param("miner_name")
|
||||||
|
|
@ -485,7 +487,51 @@ func (s *Service) handleGetMinerLogs(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logs := miner.GetLogs()
|
logs := miner.GetLogs()
|
||||||
c.JSON(http.StatusOK, logs)
|
// Base64 encode each log line to preserve ANSI escape codes and special characters
|
||||||
|
encodedLogs := make([]string, len(logs))
|
||||||
|
for i, line := range logs {
|
||||||
|
encodedLogs[i] = base64.StdEncoding.EncodeToString([]byte(line))
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, encodedLogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdinInput represents input to send to miner's stdin
|
||||||
|
type StdinInput struct {
|
||||||
|
Input string `json:"input" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMinerStdin godoc
|
||||||
|
// @Summary Send input to miner stdin
|
||||||
|
// @Description Send console commands to a running miner's stdin (e.g., 'h' for hashrate, 'p' for pause)
|
||||||
|
// @Tags miners
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param miner_name path string true "Miner Name"
|
||||||
|
// @Param input body StdinInput true "Input to send"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Router /miners/{miner_name}/stdin [post]
|
||||||
|
func (s *Service) handleMinerStdin(c *gin.Context) {
|
||||||
|
minerName := c.Param("miner_name")
|
||||||
|
miner, err := s.Manager.GetMiner(minerName)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "miner not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input StdinInput
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := miner.WriteStdin(input.Input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "sent", "input": input.Input})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleListProfiles godoc
|
// handleListProfiles godoc
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,13 @@ func (m *TTMiner) Start(config *Config) error {
|
||||||
|
|
||||||
m.cmd = exec.Command(m.MinerBinary, args...)
|
m.cmd = exec.Command(m.MinerBinary, args...)
|
||||||
|
|
||||||
|
// Create stdin pipe for console commands
|
||||||
|
stdinPipe, err := m.cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
m.stdinPipe = stdinPipe
|
||||||
|
|
||||||
// Always capture output to LogBuffer
|
// Always capture output to LogBuffer
|
||||||
if m.LogBuffer != nil {
|
if m.LogBuffer != nil {
|
||||||
m.cmd.Stdout = m.LogBuffer
|
m.cmd.Stdout = m.LogBuffer
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,13 @@ func (m *XMRigMiner) Start(config *Config) error {
|
||||||
|
|
||||||
m.cmd = exec.Command(m.MinerBinary, args...)
|
m.cmd = exec.Command(m.MinerBinary, args...)
|
||||||
|
|
||||||
|
// Create stdin pipe for console commands
|
||||||
|
stdinPipe, err := m.cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
m.stdinPipe = stdinPipe
|
||||||
|
|
||||||
// Always capture output to LogBuffer
|
// Always capture output to LogBuffer
|
||||||
if m.LogBuffer != nil {
|
if m.LogBuffer != nil {
|
||||||
m.cmd.Stdout = m.LogBuffer
|
m.cmd.Stdout = m.LogBuffer
|
||||||
|
|
|
||||||
251
ui/e2e/ui/console.e2e.spec.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { MainLayoutPage } from '../page-objects/main-layout.page';
|
||||||
|
import { ConsolePage } from '../page-objects/console.page';
|
||||||
|
|
||||||
|
test.describe('Console Page', () => {
|
||||||
|
let layout: MainLayoutPage;
|
||||||
|
let consolePage: ConsolePage;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
layout = new MainLayoutPage(page);
|
||||||
|
consolePage = new ConsolePage(page);
|
||||||
|
await layout.goto();
|
||||||
|
await layout.waitForLayoutLoad();
|
||||||
|
await layout.navigateToConsole();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Console Layout', () => {
|
||||||
|
test('should display console page container', async () => {
|
||||||
|
await expect(consolePage.consolePage).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display console header', async () => {
|
||||||
|
await expect(consolePage.tabsContainer).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display console output area', async () => {
|
||||||
|
await expect(consolePage.consoleOutput).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display console controls', async () => {
|
||||||
|
await expect(consolePage.autoScrollCheckbox).toBeVisible();
|
||||||
|
await expect(consolePage.clearButton).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Worker Selection', () => {
|
||||||
|
test('should show worker dropdown when miners are running', async ({ page }) => {
|
||||||
|
// Check if there's a worker select dropdown or no miners message
|
||||||
|
const workerSelect = page.locator('.worker-select');
|
||||||
|
const noMinersMsg = page.locator('.no-miners-msg');
|
||||||
|
|
||||||
|
// Either worker select or no miners message should be visible
|
||||||
|
const hasWorkerSelect = await workerSelect.isVisible();
|
||||||
|
const hasNoMiners = await noMinersMsg.isVisible();
|
||||||
|
|
||||||
|
expect(hasWorkerSelect || hasNoMiners).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should auto-select first miner on load', async ({ page }) => {
|
||||||
|
const workerSelect = page.locator('.worker-select');
|
||||||
|
|
||||||
|
if (await workerSelect.isVisible()) {
|
||||||
|
// A miner should be selected (value should not be empty)
|
||||||
|
const selectedValue = await workerSelect.inputValue();
|
||||||
|
expect(selectedValue).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should display miner tabs when multiple miners running', async ({ page }) => {
|
||||||
|
// This test checks if tabs appear when there are multiple miners
|
||||||
|
// We just verify the tabs container exists in the header
|
||||||
|
const consoleTabs = page.locator('.console-tabs');
|
||||||
|
// Tabs only show when multiple miners, so this may or may not be visible
|
||||||
|
// Just ensure no errors
|
||||||
|
await consolePage.tabsContainer.isVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Console Output', () => {
|
||||||
|
test('should display logs when miner is running', async ({ page }) => {
|
||||||
|
const workerSelect = page.locator('.worker-select');
|
||||||
|
|
||||||
|
if (await workerSelect.isVisible()) {
|
||||||
|
// Wait for logs to load (poll happens every 2 seconds)
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Check if logs are displayed or waiting message
|
||||||
|
const logLines = await consolePage.getLogLineCount();
|
||||||
|
const waitingMsg = page.getByText(/Waiting for logs from/);
|
||||||
|
const hasWaitingMsg = await waitingMsg.isVisible();
|
||||||
|
|
||||||
|
// Either logs should be present or waiting message
|
||||||
|
expect(logLines > 0 || hasWaitingMsg).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show empty state when no miners running', async ({ page }) => {
|
||||||
|
const noMinersMsg = page.locator('.no-miners-msg');
|
||||||
|
|
||||||
|
if (await noMinersMsg.isVisible()) {
|
||||||
|
// When no miners, empty state should show
|
||||||
|
const emptyState = consolePage.emptyState;
|
||||||
|
await expect(emptyState).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should style error lines correctly', async ({ page }) => {
|
||||||
|
// Wait for logs
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const errorLines = page.locator('.log-line.error');
|
||||||
|
const errorCount = await errorLines.count();
|
||||||
|
|
||||||
|
// If there are error lines, verify they have error styling
|
||||||
|
if (errorCount > 0) {
|
||||||
|
const firstError = errorLines.first();
|
||||||
|
await expect(firstError).toHaveClass(/error/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should style warning lines correctly', async ({ page }) => {
|
||||||
|
// Wait for logs
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const warningLines = page.locator('.log-line.warning');
|
||||||
|
const warningCount = await warningLines.count();
|
||||||
|
|
||||||
|
// If there are warning lines, verify they have warning styling
|
||||||
|
if (warningCount > 0) {
|
||||||
|
const firstWarning = warningLines.first();
|
||||||
|
await expect(firstWarning).toHaveClass(/warning/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Console Controls', () => {
|
||||||
|
test('should have auto-scroll enabled by default', async () => {
|
||||||
|
const isEnabled = await consolePage.isAutoScrollEnabled();
|
||||||
|
expect(isEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should toggle auto-scroll when checkbox clicked', async () => {
|
||||||
|
// Initially enabled
|
||||||
|
expect(await consolePage.isAutoScrollEnabled()).toBe(true);
|
||||||
|
|
||||||
|
// Toggle off
|
||||||
|
await consolePage.toggleAutoScroll();
|
||||||
|
expect(await consolePage.isAutoScrollEnabled()).toBe(false);
|
||||||
|
|
||||||
|
// Toggle back on
|
||||||
|
await consolePage.toggleAutoScroll();
|
||||||
|
expect(await consolePage.isAutoScrollEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clear logs when clear button clicked', async ({ page }) => {
|
||||||
|
const workerSelect = page.locator('.worker-select');
|
||||||
|
|
||||||
|
if (await workerSelect.isVisible()) {
|
||||||
|
// Wait for logs to load
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const initialLogCount = await consolePage.getLogLineCount();
|
||||||
|
|
||||||
|
if (initialLogCount > 0) {
|
||||||
|
// Clear logs
|
||||||
|
await consolePage.clearLogs();
|
||||||
|
|
||||||
|
// Verify logs are cleared
|
||||||
|
const finalLogCount = await consolePage.getLogLineCount();
|
||||||
|
expect(finalLogCount).toBe(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable clear button when no logs', async ({ page }) => {
|
||||||
|
const workerSelect = page.locator('.worker-select');
|
||||||
|
|
||||||
|
if (await workerSelect.isVisible()) {
|
||||||
|
// Wait for logs
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const logCount = await consolePage.getLogLineCount();
|
||||||
|
|
||||||
|
if (logCount > 0) {
|
||||||
|
// Clear logs
|
||||||
|
await consolePage.clearLogs();
|
||||||
|
|
||||||
|
// Clear button should be disabled now
|
||||||
|
const isEnabled = await consolePage.isClearButtonEnabled();
|
||||||
|
expect(isEnabled).toBe(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Log Polling', () => {
|
||||||
|
test('should update logs periodically', async ({ page }) => {
|
||||||
|
const workerSelect = page.locator('.worker-select');
|
||||||
|
|
||||||
|
if (await workerSelect.isVisible()) {
|
||||||
|
// Wait for initial logs
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const initialLogs = await consolePage.getLogContent();
|
||||||
|
|
||||||
|
// Wait for another poll cycle (2+ seconds)
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Check if new logs appeared (miner generates output)
|
||||||
|
// We can't guarantee new logs, but verify no errors
|
||||||
|
const finalLogs = await consolePage.getLogContent();
|
||||||
|
|
||||||
|
// Logs array should still be valid
|
||||||
|
expect(Array.isArray(finalLogs)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Worker Switching', () => {
|
||||||
|
test('should switch miner and clear logs when selection changes', async ({ page }) => {
|
||||||
|
const workerSelect = page.locator('.worker-select');
|
||||||
|
|
||||||
|
if (await workerSelect.isVisible()) {
|
||||||
|
// Get available options
|
||||||
|
const options = await workerSelect.locator('option').allTextContents();
|
||||||
|
|
||||||
|
if (options.length > 1) {
|
||||||
|
// Wait for initial logs
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
// Get initial selected value
|
||||||
|
const initialValue = await workerSelect.inputValue();
|
||||||
|
|
||||||
|
// Find a different miner to select
|
||||||
|
const otherMiner = options.find(opt => opt !== initialValue);
|
||||||
|
|
||||||
|
if (otherMiner) {
|
||||||
|
// Select different miner
|
||||||
|
await workerSelect.selectOption(otherMiner);
|
||||||
|
|
||||||
|
// Logs should be cleared momentarily (new miner selected)
|
||||||
|
// Then new logs should load
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Responsive Behavior', () => {
|
||||||
|
test('should maintain layout when resized', async ({ page }) => {
|
||||||
|
// Resize to smaller viewport
|
||||||
|
await page.setViewportSize({ width: 800, height: 600 });
|
||||||
|
|
||||||
|
// Console should still be visible and functional
|
||||||
|
await expect(consolePage.consolePage).toBeVisible();
|
||||||
|
await expect(consolePage.consoleOutput).toBeVisible();
|
||||||
|
await expect(consolePage.clearButton).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "Quick Test 1767041846199"
|
|
||||||
- option "Mining Test 1767041844401"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-cancel-1767041832358"
|
|
||||||
- option "Long Test 1767041847141"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-cancel-1767041832358"
|
|
||||||
- option "Long Test 1767041847141"
|
|
||||||
- option "E2E List Test Profile"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- option "Quick Test 1767041846199"
|
|
||||||
- option "Mining Test 1767041844401"
|
|
||||||
- option "E2E Delete Test Profile"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e6]:
|
|
||||||
- img [ref=e7]
|
|
||||||
- text: Setup Required
|
|
||||||
- paragraph [ref=e9]: To begin, please install a miner from the list below.
|
|
||||||
- heading "Available Miners" [level=4] [ref=e10]
|
|
||||||
- generic [ref=e11]:
|
|
||||||
- generic [ref=e12]:
|
|
||||||
- text: xmrig
|
|
||||||
- button "Install" [ref=e13]:
|
|
||||||
- img [ref=e14]
|
|
||||||
- text: Install
|
|
||||||
- generic [ref=e16]:
|
|
||||||
- text: tt-miner
|
|
||||||
- button "Install" [ref=e17]:
|
|
||||||
- img [ref=e18]
|
|
||||||
- text: Install
|
|
||||||
```
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- option "Quick Test 1767041846199"
|
|
||||||
- option "Mining Test 1767041844401"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-cancel-1767041832358"
|
|
||||||
- option "Long Test 1767041847141"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-cancel-1767041832358"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "E2E Test 1767041854325"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
|
@ -1,74 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 8s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "1"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (1)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (1)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- generic [ref=e92]: "Worker:"
|
|
||||||
- combobox [ref=e93] [cursor=pointer]:
|
|
||||||
- option "xmrig-279" [selected]
|
|
||||||
- paragraph [ref=e96]: Waiting for logs from xmrig-279...
|
|
||||||
- generic [ref=e97]:
|
|
||||||
- generic [ref=e98] [cursor=pointer]:
|
|
||||||
- checkbox "Auto-scroll" [checked] [ref=e99]
|
|
||||||
- generic [ref=e100]: Auto-scroll
|
|
||||||
- button "Clear" [disabled] [ref=e101]:
|
|
||||||
- img
|
|
||||||
- text: Clear
|
|
||||||
```
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 32 KiB |
|
|
@ -1,68 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
|
@ -1,114 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 1s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "2"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (2)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (2)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e90]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-cancel-1767041832358"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- option "Quick Test 1767041846199"
|
|
||||||
- option "Mining Test 1767041844401"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- button "Stop All" [ref=e95] [cursor=pointer]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- text: Stop All
|
|
||||||
- table [ref=e100]:
|
|
||||||
- rowgroup [ref=e101]:
|
|
||||||
- row "Worker Hashrate Shares Efficiency Uptime Pool Actions" [ref=e102]:
|
|
||||||
- columnheader "Worker" [ref=e103]
|
|
||||||
- columnheader "Hashrate" [ref=e104]
|
|
||||||
- columnheader "Shares" [ref=e105]
|
|
||||||
- columnheader "Efficiency" [ref=e106]
|
|
||||||
- columnheader "Uptime" [ref=e107]
|
|
||||||
- columnheader "Pool" [ref=e108]
|
|
||||||
- columnheader "Actions" [ref=e109]
|
|
||||||
- rowgroup [ref=e110]:
|
|
||||||
- row "xmrig-607 0H/s 0 100.0% 1s N/A" [ref=e111]:
|
|
||||||
- cell "xmrig-607" [ref=e112]:
|
|
||||||
- generic [ref=e115]: xmrig-607
|
|
||||||
- cell "0H/s" [ref=e116]: 0H/s
|
|
||||||
- cell "0" [ref=e118]
|
|
||||||
- cell "100.0%" [ref=e119]
|
|
||||||
- cell "1s" [ref=e120]
|
|
||||||
- cell "N/A" [ref=e121]
|
|
||||||
- cell [ref=e122]:
|
|
||||||
- button "View logs" [ref=e123] [cursor=pointer]:
|
|
||||||
- img [ref=e124]
|
|
||||||
- button "Stop worker" [ref=e126] [cursor=pointer]:
|
|
||||||
- img [ref=e127]
|
|
||||||
- row "xmrig-51 0H/s 0 100.0% 0s N/A" [ref=e129]:
|
|
||||||
- cell "xmrig-51" [ref=e130]:
|
|
||||||
- generic [ref=e133]: xmrig-51
|
|
||||||
- cell "0H/s" [ref=e134]: 0H/s
|
|
||||||
- cell "0" [ref=e136]
|
|
||||||
- cell "100.0%" [ref=e137]
|
|
||||||
- cell "0s" [ref=e138]
|
|
||||||
- cell "N/A" [ref=e139]
|
|
||||||
- cell [ref=e140]:
|
|
||||||
- button "View logs" [ref=e141] [cursor=pointer]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- button "Stop worker" [ref=e144] [cursor=pointer]:
|
|
||||||
- img [ref=e145]
|
|
||||||
```
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-cancel-1767041832358"
|
|
||||||
- option "Long Test 1767041847141"
|
|
||||||
- option "E2E List Test Profile"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- option "Quick Test 1767041846199"
|
|
||||||
- option "Mining Test 1767041844401"
|
|
||||||
- option "E2E Delete Test Profile"
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-cancel-1767041832358"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
|
@ -1,74 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- option "FT-display-1767041826192"
|
|
||||||
- option "FT-delete-1767041826329"
|
|
||||||
- option "FT-edit-1767041826330"
|
|
||||||
- option "FT-start-1767041826205"
|
|
||||||
- option "FT-editform-1767041826397"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# Page snapshot
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- complementary [ref=e7]:
|
|
||||||
- generic [ref=e8]:
|
|
||||||
- generic [ref=e9]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e11]: Mining
|
|
||||||
- button [ref=e12] [cursor=pointer]:
|
|
||||||
- img [ref=e13]
|
|
||||||
- navigation [ref=e15]:
|
|
||||||
- button "Workers" [ref=e16] [cursor=pointer]:
|
|
||||||
- generic [ref=e18]: Workers
|
|
||||||
- button "Graphs" [ref=e19] [cursor=pointer]:
|
|
||||||
- generic [ref=e21]: Graphs
|
|
||||||
- button "Console" [ref=e22] [cursor=pointer]:
|
|
||||||
- generic [ref=e24]: Console
|
|
||||||
- button "Pools" [ref=e25] [cursor=pointer]:
|
|
||||||
- generic [ref=e27]: Pools
|
|
||||||
- button "Profiles" [ref=e28] [cursor=pointer]:
|
|
||||||
- generic [ref=e30]: Profiles
|
|
||||||
- button "Miners" [ref=e31] [cursor=pointer]:
|
|
||||||
- generic [ref=e33]: Miners
|
|
||||||
- generic [ref=e37]: Mining Active
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]:
|
|
||||||
- img [ref=e43]
|
|
||||||
- generic [ref=e45]:
|
|
||||||
- generic [ref=e46]: "0"
|
|
||||||
- generic [ref=e47]: H/s
|
|
||||||
- generic [ref=e48]: Hashrate
|
|
||||||
- generic [ref=e50]:
|
|
||||||
- img [ref=e51]
|
|
||||||
- generic [ref=e54]: "0"
|
|
||||||
- generic [ref=e55]: Shares
|
|
||||||
- generic [ref=e57]:
|
|
||||||
- img [ref=e58]
|
|
||||||
- generic [ref=e61]: 0s
|
|
||||||
- generic [ref=e62]: Uptime
|
|
||||||
- generic [ref=e64]:
|
|
||||||
- img [ref=e65]
|
|
||||||
- generic [ref=e68]: Not connected
|
|
||||||
- generic [ref=e69]: Pool
|
|
||||||
- generic [ref=e71]:
|
|
||||||
- img [ref=e72]
|
|
||||||
- generic [ref=e75]: "0"
|
|
||||||
- generic [ref=e76]: Workers
|
|
||||||
- button "All Workers (0)" [ref=e79] [cursor=pointer]:
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e83]: All Workers
|
|
||||||
- generic [ref=e84]: (0)
|
|
||||||
- img [ref=e85]
|
|
||||||
- generic [ref=e89]:
|
|
||||||
- generic [ref=e91]:
|
|
||||||
- combobox [ref=e92]:
|
|
||||||
- option "Select profile..." [disabled] [selected]
|
|
||||||
- option "Mining Test 1767031630070"
|
|
||||||
- button "Start" [disabled] [ref=e93]:
|
|
||||||
- img
|
|
||||||
- text: Start
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- heading "No Active Workers" [level=3] [ref=e98]
|
|
||||||
- paragraph [ref=e99]: Select a profile and start mining to see workers here.
|
|
||||||
```
|
|
||||||
|
|
@ -1,29 +1,24 @@
|
||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { MainLayoutComponent } from './layouts/main-layout.component';
|
|
||||||
import { WorkersComponent } from './pages/workers/workers.component';
|
import { WorkersComponent } from './pages/workers/workers.component';
|
||||||
import { GraphsComponent } from './pages/graphs/graphs.component';
|
import { GraphsComponent } from './pages/graphs/graphs.component';
|
||||||
import { ConsoleComponent } from './pages/console/console.component';
|
import { ConsoleComponent } from './pages/console/console.component';
|
||||||
import { PoolsComponent } from './pages/pools/pools.component';
|
import { PoolsComponent } from './pages/pools/pools.component';
|
||||||
import { ProfilesComponent } from './pages/profiles/profiles.component';
|
import { ProfilesComponent } from './pages/profiles/profiles.component';
|
||||||
import { MinersComponent } from './pages/miners/miners.component';
|
import { MinersComponent } from './pages/miners/miners.component';
|
||||||
|
import { NodesComponent } from './pages/nodes/nodes.component';
|
||||||
import { SystemTrayComponent } from './pages/system-tray/system-tray.component';
|
import { SystemTrayComponent } from './pages/system-tray/system-tray.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
// System tray is standalone without layout
|
// System tray is standalone without layout
|
||||||
{ path: 'system-tray', component: SystemTrayComponent },
|
{ path: 'system-tray', component: SystemTrayComponent },
|
||||||
|
|
||||||
// All other routes use the main layout
|
// Main app routes - MainLayoutComponent is rendered directly and contains router-outlet
|
||||||
{
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
path: '',
|
{ path: 'dashboard', component: GraphsComponent },
|
||||||
component: MainLayoutComponent,
|
{ path: 'workers', component: WorkersComponent },
|
||||||
children: [
|
{ path: 'console', component: ConsoleComponent },
|
||||||
{ path: '', redirectTo: 'workers', pathMatch: 'full' },
|
{ path: 'pools', component: PoolsComponent },
|
||||||
{ path: 'workers', component: WorkersComponent },
|
{ path: 'profiles', component: ProfilesComponent },
|
||||||
{ path: 'graphs', component: GraphsComponent },
|
{ path: 'miners', component: MinersComponent },
|
||||||
{ path: 'console', component: ConsoleComponent },
|
{ path: 'nodes', component: NodesComponent },
|
||||||
{ path: 'pools', component: PoolsComponent },
|
|
||||||
{ path: 'profiles', component: ProfilesComponent },
|
|
||||||
{ path: 'miners', component: MinersComponent },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { MainLayoutComponent } from './layouts/main-layout.component';
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SetupWizardComponent,
|
SetupWizardComponent,
|
||||||
MainLayoutComponent
|
MainLayoutComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrls: ['./app.css'],
|
styleUrls: ['./app.css'],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,55 @@
|
||||||
.chart-container {
|
.chart-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-btn {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #94a3b8;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-btn:hover {
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: rgba(148, 163, 184, 0.1);
|
||||||
|
border-color: rgba(148, 163, 184, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-range-btn.active {
|
||||||
|
color: var(--color-accent-400, #00d4ff);
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
border-color: var(--color-accent-500, #00d4ff);
|
||||||
|
}
|
||||||
|
|
||||||
.hashrate-chart {
|
.hashrate-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,17 @@
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
|
<div class="chart-header">
|
||||||
|
<h3 class="chart-title">Hashrate History</h3>
|
||||||
|
<div class="time-range-selector">
|
||||||
|
@for (range of minerService.timeRanges; track range.minutes) {
|
||||||
|
<button
|
||||||
|
class="time-range-btn"
|
||||||
|
[class.active]="minerService.selectedTimeRange() === range.minutes"
|
||||||
|
(click)="minerService.setTimeRange(range.minutes)">
|
||||||
|
{{ range.label }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<highcharts-chart
|
<highcharts-chart
|
||||||
class="hashrate-chart"
|
class="hashrate-chart"
|
||||||
[Highcharts]="Highcharts"
|
[Highcharts]="Highcharts"
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ type SeriesWithData = Highcharts.SeriesAreaOptions | Highcharts.SeriesSplineOpti
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class ChartComponent {
|
export class ChartComponent {
|
||||||
private minerService = inject(MinerService);
|
minerService = inject(MinerService); // Public for template access
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
Highcharts: typeof Highcharts = Highcharts;
|
Highcharts: typeof Highcharts = Highcharts;
|
||||||
|
|
@ -60,7 +60,7 @@ export class ChartComponent {
|
||||||
...this.createBaseChartOptions().chart,
|
...this.createBaseChartOptions().chart,
|
||||||
type: 'area'
|
type: 'area'
|
||||||
},
|
},
|
||||||
title: { text: 'Total Hashrate' },
|
title: { text: '' },
|
||||||
plotOptions: {
|
plotOptions: {
|
||||||
area: {
|
area: {
|
||||||
stacking: 'normal',
|
stacking: 'normal',
|
||||||
|
|
@ -74,12 +74,8 @@ export class ChartComponent {
|
||||||
|
|
||||||
// Create effect with proper cleanup
|
// Create effect with proper cleanup
|
||||||
const effectRef = effect(() => {
|
const effectRef = effect(() => {
|
||||||
const historyMap = this.minerService.hashrateHistory();
|
// Use 24-hour historical data from database
|
||||||
|
const historyMap = this.minerService.historicalHashrate();
|
||||||
// Skip if no data
|
|
||||||
if (historyMap.size === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up colors for miners no longer active
|
// Clean up colors for miners no longer active
|
||||||
const activeNames = new Set(historyMap.keys());
|
const activeNames = new Set(historyMap.keys());
|
||||||
|
|
@ -107,7 +103,7 @@ export class ChartComponent {
|
||||||
// Build new chart options
|
// Build new chart options
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
...this.createBaseChartOptions(),
|
...this.createBaseChartOptions(),
|
||||||
title: { text: 'Total Hashrate' },
|
title: { text: '' },
|
||||||
chart: {
|
chart: {
|
||||||
...this.createBaseChartOptions().chart,
|
...this.createBaseChartOptions().chart,
|
||||||
type: 'area'
|
type: 'area'
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { Component, signal, output, input } from '@angular/core';
|
import { Component, signal, output, input, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: SafeHtml;
|
||||||
route: string;
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,55 +211,61 @@ interface NavItem {
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class SidebarComponent {
|
export class SidebarComponent {
|
||||||
|
private sanitizer = inject(DomSanitizer);
|
||||||
|
|
||||||
collapsed = signal(false);
|
collapsed = signal(false);
|
||||||
currentRoute = input<string>('workers');
|
currentRoute = input<string>('dashboard');
|
||||||
routeChange = output<string>();
|
routeChange = output<string>();
|
||||||
|
|
||||||
navItems: NavItem[] = [
|
navItems: NavItem[] = [
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
label: 'Dashboard',
|
||||||
|
route: 'dashboard',
|
||||||
|
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'workers',
|
id: 'workers',
|
||||||
label: 'Workers',
|
label: 'Workers',
|
||||||
route: 'workers',
|
route: 'workers',
|
||||||
icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>'
|
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>')
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'graphs',
|
|
||||||
label: 'Graphs',
|
|
||||||
route: 'graphs',
|
|
||||||
icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'console',
|
id: 'console',
|
||||||
label: 'Console',
|
label: 'Console',
|
||||||
route: 'console',
|
route: 'console',
|
||||||
icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>'
|
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pools',
|
id: 'pools',
|
||||||
label: 'Pools',
|
label: 'Pools',
|
||||||
route: 'pools',
|
route: 'pools',
|
||||||
icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>'
|
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'profiles',
|
id: 'profiles',
|
||||||
label: 'Profiles',
|
label: 'Profiles',
|
||||||
route: 'profiles',
|
route: 'profiles',
|
||||||
icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>'
|
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'miners',
|
id: 'miners',
|
||||||
label: 'Miners',
|
label: 'Miners',
|
||||||
route: 'miners',
|
route: 'miners',
|
||||||
icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>'
|
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'nodes',
|
id: 'nodes',
|
||||||
label: 'Nodes',
|
label: 'Nodes',
|
||||||
route: 'nodes',
|
route: 'nodes',
|
||||||
icon: '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01M9 12h.01M12 12h.01M15 12h.01"/></svg>'
|
icon: this.trustIcon('<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>')
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private trustIcon(svg: string): SafeHtml {
|
||||||
|
return this.sanitizer.bypassSecurityTrustHtml(svg);
|
||||||
|
}
|
||||||
|
|
||||||
toggleCollapse() {
|
toggleCollapse() {
|
||||||
this.collapsed.update(v => !v);
|
this.collapsed.update(v => !v);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,21 @@
|
||||||
import { Component, signal } from '@angular/core';
|
import { Component, inject, AfterViewInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
|
||||||
|
import { filter, map } from 'rxjs/operators';
|
||||||
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { SidebarComponent } from '../components/sidebar/sidebar.component';
|
import { SidebarComponent } from '../components/sidebar/sidebar.component';
|
||||||
import { StatsPanelComponent } from '../components/stats-panel/stats-panel.component';
|
import { StatsPanelComponent } from '../components/stats-panel/stats-panel.component';
|
||||||
import { MinerSwitcherComponent } from '../components/miner-switcher/miner-switcher.component';
|
import { MinerSwitcherComponent } from '../components/miner-switcher/miner-switcher.component';
|
||||||
import { WorkersComponent } from '../pages/workers/workers.component';
|
|
||||||
import { GraphsComponent } from '../pages/graphs/graphs.component';
|
|
||||||
import { ConsoleComponent } from '../pages/console/console.component';
|
|
||||||
import { PoolsComponent } from '../pages/pools/pools.component';
|
|
||||||
import { ProfilesComponent } from '../pages/profiles/profiles.component';
|
|
||||||
import { MinersComponent } from '../pages/miners/miners.component';
|
|
||||||
import { NodesComponent } from '../pages/nodes/nodes.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-main-layout',
|
selector: 'app-main-layout',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
RouterOutlet,
|
||||||
SidebarComponent,
|
SidebarComponent,
|
||||||
StatsPanelComponent,
|
StatsPanelComponent,
|
||||||
MinerSwitcherComponent,
|
MinerSwitcherComponent,
|
||||||
WorkersComponent,
|
|
||||||
GraphsComponent,
|
|
||||||
ConsoleComponent,
|
|
||||||
PoolsComponent,
|
|
||||||
ProfilesComponent,
|
|
||||||
MinersComponent,
|
|
||||||
NodesComponent
|
|
||||||
],
|
],
|
||||||
template: `
|
template: `
|
||||||
<div class="main-layout">
|
<div class="main-layout">
|
||||||
|
|
@ -38,32 +28,7 @@ import { NodesComponent } from '../pages/nodes/nodes.component';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
@switch (currentRoute()) {
|
<router-outlet></router-outlet>
|
||||||
@case ('workers') {
|
|
||||||
<app-workers></app-workers>
|
|
||||||
}
|
|
||||||
@case ('graphs') {
|
|
||||||
<app-graphs></app-graphs>
|
|
||||||
}
|
|
||||||
@case ('console') {
|
|
||||||
<app-console></app-console>
|
|
||||||
}
|
|
||||||
@case ('pools') {
|
|
||||||
<app-pools></app-pools>
|
|
||||||
}
|
|
||||||
@case ('profiles') {
|
|
||||||
<app-profiles></app-profiles>
|
|
||||||
}
|
|
||||||
@case ('miners') {
|
|
||||||
<app-miners></app-miners>
|
|
||||||
}
|
|
||||||
@case ('nodes') {
|
|
||||||
<app-nodes></app-nodes>
|
|
||||||
}
|
|
||||||
@default {
|
|
||||||
<app-workers></app-workers>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,17 +67,42 @@ import { NodesComponent } from '../pages/nodes/nodes.component';
|
||||||
}
|
}
|
||||||
`]
|
`]
|
||||||
})
|
})
|
||||||
export class MainLayoutComponent {
|
export class MainLayoutComponent implements AfterViewInit {
|
||||||
currentRoute = signal('workers');
|
private router = inject(Router);
|
||||||
private editingProfileId: string | null = null;
|
|
||||||
|
// Track current route from router events
|
||||||
|
currentRoute = toSignal(
|
||||||
|
this.router.events.pipe(
|
||||||
|
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||||
|
map(event => {
|
||||||
|
// Extract route from URL like "/#/workers" or "/workers"
|
||||||
|
const url = event.urlAfterRedirects;
|
||||||
|
const segments = url.split('/').filter(s => s && s !== '#');
|
||||||
|
return segments[0] || 'dashboard';
|
||||||
|
})
|
||||||
|
),
|
||||||
|
{ initialValue: this.getInitialRoute() }
|
||||||
|
);
|
||||||
|
|
||||||
|
private getInitialRoute(): string {
|
||||||
|
const url = this.router.url;
|
||||||
|
const segments = url.split('/').filter(s => s && s !== '#');
|
||||||
|
return segments[0] || 'dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
// Re-trigger navigation after router-outlet is available
|
||||||
|
// This handles the case where router tried to navigate before outlet existed
|
||||||
|
const route = this.getInitialRoute();
|
||||||
|
setTimeout(() => this.router.navigate(['/', route]), 0);
|
||||||
|
}
|
||||||
|
|
||||||
onRouteChange(route: string) {
|
onRouteChange(route: string) {
|
||||||
this.currentRoute.set(route);
|
this.router.navigate(['/', route]);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToProfiles(profileId: string) {
|
navigateToProfiles(profileId: string) {
|
||||||
this.editingProfileId = profileId;
|
// TODO: Could pass profileId via query params or state
|
||||||
this.currentRoute.set('profiles');
|
this.router.navigate(['/', 'profiles']);
|
||||||
// TODO: Could emit event to profiles page to open edit modal for this profile
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,24 @@ export class MinerService implements OnDestroy {
|
||||||
// Separate signal for hashrate history as it updates frequently
|
// Separate signal for hashrate history as it updates frequently
|
||||||
public hashrateHistory = signal<Map<string, HashratePoint[]>>(new Map());
|
public hashrateHistory = signal<Map<string, HashratePoint[]>>(new Map());
|
||||||
|
|
||||||
|
// Historical hashrate data from database with configurable time range
|
||||||
|
public historicalHashrate = signal<Map<string, HashratePoint[]>>(new Map());
|
||||||
|
public selectedTimeRange = signal<number>(60); // Default 60 minutes
|
||||||
|
private historyPollingSubscription?: Subscription;
|
||||||
|
|
||||||
|
// Available time ranges in minutes
|
||||||
|
public readonly timeRanges = [
|
||||||
|
{ label: '5m', minutes: 5 },
|
||||||
|
{ label: '15m', minutes: 15 },
|
||||||
|
{ label: '30m', minutes: 30 },
|
||||||
|
{ label: '45m', minutes: 45 },
|
||||||
|
{ label: '1h', minutes: 60 },
|
||||||
|
{ label: '3h', minutes: 180 },
|
||||||
|
{ label: '6h', minutes: 360 },
|
||||||
|
{ label: '12h', minutes: 720 },
|
||||||
|
{ label: '24h', minutes: 1440 },
|
||||||
|
];
|
||||||
|
|
||||||
// --- View Mode Signals (single/multi miner view) ---
|
// --- View Mode Signals (single/multi miner view) ---
|
||||||
public viewMode = signal<'all' | 'single'>('all');
|
public viewMode = signal<'all' | 'single'>('all');
|
||||||
public selectedMinerName = signal<string | null>(null);
|
public selectedMinerName = signal<string | null>(null);
|
||||||
|
|
@ -92,10 +110,12 @@ export class MinerService implements OnDestroy {
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
this.forceRefreshState();
|
this.forceRefreshState();
|
||||||
this.startPollingLive_Data();
|
this.startPollingLive_Data();
|
||||||
|
this.startPollingHistoricalData();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
|
this.historyPollingSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Data Loading and Polling Logic ---
|
// --- Data Loading and Polling Logic ---
|
||||||
|
|
@ -116,6 +136,8 @@ export class MinerService implements OnDestroy {
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
this.state.set(initialState);
|
this.state.set(initialState);
|
||||||
this.updateHashrateHistory(initialState.runningMiners);
|
this.updateHashrateHistory(initialState.runningMiners);
|
||||||
|
// Fetch historical data now that we know which miners are running
|
||||||
|
this.fetchHistoricalHashrate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +154,66 @@ export class MinerService implements OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a polling interval to fetch historical data from database.
|
||||||
|
* Polls every 30 seconds. Initial fetch happens in forceRefreshState after miners are loaded.
|
||||||
|
*/
|
||||||
|
private startPollingHistoricalData() {
|
||||||
|
// Poll every 30 seconds (initial fetch happens in forceRefreshState)
|
||||||
|
this.historyPollingSubscription = interval(30000).subscribe(() => {
|
||||||
|
this.fetchHistoricalHashrate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches 24-hour historical hashrate data for all running miners from the database.
|
||||||
|
*/
|
||||||
|
private fetchHistoricalHashrate() {
|
||||||
|
const runningMiners = this.state().runningMiners;
|
||||||
|
if (runningMiners.length === 0) {
|
||||||
|
this.historicalHashrate.set(new Map());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch historical data for each running miner
|
||||||
|
const requests = runningMiners.map(miner =>
|
||||||
|
this.getHistoricalHashrateForMiner(miner.name).pipe(
|
||||||
|
map(data => ({ name: miner.name, data })),
|
||||||
|
catchError(() => of({ name: miner.name, data: [] as HashratePoint[] }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
forkJoin(requests).subscribe(results => {
|
||||||
|
const newHistory = new Map<string, HashratePoint[]>();
|
||||||
|
results.forEach(result => {
|
||||||
|
if (result.data && result.data.length > 0) {
|
||||||
|
newHistory.set(result.name, result.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.historicalHashrate.set(newHistory);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches historical hashrate for a specific miner based on selected time range.
|
||||||
|
*/
|
||||||
|
private getHistoricalHashrateForMiner(minerName: string) {
|
||||||
|
const minutes = this.selectedTimeRange();
|
||||||
|
const since = new Date(Date.now() - minutes * 60 * 1000).toISOString();
|
||||||
|
const until = new Date().toISOString();
|
||||||
|
return this.http.get<HashratePoint[]>(
|
||||||
|
`${this.apiBaseUrl}/history/miners/${minerName}/hashrate?since=${since}&until=${until}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the time range for historical data and refreshes immediately.
|
||||||
|
*/
|
||||||
|
public setTimeRange(minutes: number) {
|
||||||
|
this.selectedTimeRange.set(minutes);
|
||||||
|
this.fetchHistoricalHashrate();
|
||||||
|
}
|
||||||
|
|
||||||
private stopPolling() {
|
private stopPolling() {
|
||||||
this.pollingSubscription?.unsubscribe();
|
this.pollingSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +267,24 @@ export class MinerService implements OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMinerLogs(minerName: string) {
|
getMinerLogs(minerName: string) {
|
||||||
return this.http.get<string[]>(`${this.apiBaseUrl}/miners/${minerName}/logs`);
|
return this.http.get<string[]>(`${this.apiBaseUrl}/miners/${minerName}/logs`).pipe(
|
||||||
|
map(logs => logs.map(line => {
|
||||||
|
try {
|
||||||
|
// Decode base64 encoded log lines
|
||||||
|
return atob(line);
|
||||||
|
} catch {
|
||||||
|
// If decoding fails, return the original line
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStdin(minerName: string, input: string) {
|
||||||
|
return this.http.post<{status: string, input: string}>(
|
||||||
|
`${this.apiBaseUrl}/miners/${minerName}/stdin`,
|
||||||
|
{ input }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createProfile(profile: MiningProfile) {
|
createProfile(profile: MiningProfile) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, inject, computed, signal, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewChecked } from '@angular/core';
|
import { Component, inject, computed, signal, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewChecked } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { MinerService } from '../../miner.service';
|
import { MinerService } from '../../miner.service';
|
||||||
import { HttpClient } from '@angular/common/http';
|
|
||||||
import { interval, Subscription, switchMap } from 'rxjs';
|
import { interval, Subscription, switchMap } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -58,7 +58,7 @@ import { interval, Subscription, switchMap } from 'rxjs';
|
||||||
@if (logs().length > 0) {
|
@if (logs().length > 0) {
|
||||||
@for (line of logs(); track $index) {
|
@for (line of logs(); track $index) {
|
||||||
<div class="log-line" [class.error]="isErrorLine(line)" [class.warning]="isWarningLine(line)">
|
<div class="log-line" [class.error]="isErrorLine(line)" [class.warning]="isWarningLine(line)">
|
||||||
<span class="log-text">{{ line }}</span>
|
<span class="log-text" [innerHTML]="ansiToHtml(line)"></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} @else if (activeMiner()) {
|
} @else if (activeMiner()) {
|
||||||
|
|
@ -75,6 +75,19 @@ import { interval, Subscription, switchMap } from 'rxjs';
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Console Input -->
|
||||||
|
<div class="console-input-wrapper">
|
||||||
|
<span class="input-prompt">></span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="console-input"
|
||||||
|
placeholder="Type command (h=hashrate, p=pause, r=resume, s=results, c=connection)"
|
||||||
|
[value]="stdinInput()"
|
||||||
|
(input)="onStdinInput($event)"
|
||||||
|
(keydown.enter)="sendStdinCommand()"
|
||||||
|
[disabled]="!activeMiner()">
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Console Controls -->
|
<!-- Console Controls -->
|
||||||
<div class="console-controls">
|
<div class="console-controls">
|
||||||
<label class="control-checkbox">
|
<label class="control-checkbox">
|
||||||
|
|
@ -259,6 +272,49 @@ import { interval, Subscription, switchMap } from 'rxjs';
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.console-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: rgba(10, 10, 18, 0.6);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border-left: 1px solid rgb(37 37 66 / 0.2);
|
||||||
|
border-right: 1px solid rgb(37 37 66 / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-prompt {
|
||||||
|
color: var(--color-accent-500);
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: rgba(163, 230, 53, 0.8);
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
caret-color: var(--color-accent-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input::placeholder {
|
||||||
|
color: rgba(100, 116, 139, 0.4);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input:focus {
|
||||||
|
color: #a3e635;
|
||||||
|
}
|
||||||
|
|
||||||
.console-controls {
|
.console-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -314,7 +370,7 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
||||||
@ViewChild('consoleOutput') consoleOutput!: ElementRef;
|
@ViewChild('consoleOutput') consoleOutput!: ElementRef;
|
||||||
|
|
||||||
private minerService = inject(MinerService);
|
private minerService = inject(MinerService);
|
||||||
private http = inject(HttpClient);
|
private sanitizer = inject(DomSanitizer);
|
||||||
private state = this.minerService.state;
|
private state = this.minerService.state;
|
||||||
private pollSub?: Subscription;
|
private pollSub?: Subscription;
|
||||||
|
|
||||||
|
|
@ -337,13 +393,16 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
||||||
|
|
||||||
logs = signal<string[]>([]);
|
logs = signal<string[]>([]);
|
||||||
autoScroll = signal(true);
|
autoScroll = signal(true);
|
||||||
|
stdinInput = signal('');
|
||||||
private shouldScroll = false;
|
private shouldScroll = false;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Auto-select first miner for console view
|
// Auto-select first miner for console view and fetch logs immediately
|
||||||
const miners = this.runningMiners();
|
const miners = this.runningMiners();
|
||||||
if (miners.length > 0) {
|
if (miners.length > 0) {
|
||||||
this.consoleSelectedMiner.set(miners[0].name);
|
this.consoleSelectedMiner.set(miners[0].name);
|
||||||
|
// Fetch logs immediately - don't wait for interval
|
||||||
|
this.fetchLogs(miners[0].name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll for logs every 2 seconds
|
// Poll for logs every 2 seconds
|
||||||
|
|
@ -351,12 +410,12 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
const miner = this.activeMiner();
|
const miner = this.activeMiner();
|
||||||
if (!miner) return [];
|
if (!miner) return [];
|
||||||
return this.http.get<{logs: string[]}>(`/api/v1/mining/miners/${miner}/logs`);
|
return this.minerService.getMinerLogs(miner);
|
||||||
})
|
})
|
||||||
).subscribe({
|
).subscribe({
|
||||||
next: (response: any) => {
|
next: (logs: string[]) => {
|
||||||
if (response?.logs) {
|
if (logs && Array.isArray(logs)) {
|
||||||
this.logs.set(response.logs);
|
this.logs.set(logs);
|
||||||
if (this.autoScroll()) {
|
if (this.autoScroll()) {
|
||||||
this.shouldScroll = true;
|
this.shouldScroll = true;
|
||||||
}
|
}
|
||||||
|
|
@ -385,10 +444,10 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchLogs(minerName: string) {
|
private fetchLogs(minerName: string) {
|
||||||
this.http.get<{logs: string[]}>(`/api/v1/mining/miners/${minerName}/logs`).subscribe({
|
this.minerService.getMinerLogs(minerName).subscribe({
|
||||||
next: (response) => {
|
next: (logs) => {
|
||||||
if (response?.logs) {
|
if (logs && Array.isArray(logs)) {
|
||||||
this.logs.set(response.logs);
|
this.logs.set(logs);
|
||||||
this.shouldScroll = true;
|
this.shouldScroll = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -408,6 +467,26 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
||||||
this.selectConsoleMiner(select.value);
|
this.selectConsoleMiner(select.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onStdinInput(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
this.stdinInput.set(input.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStdinCommand() {
|
||||||
|
const miner = this.activeMiner();
|
||||||
|
const input = this.stdinInput();
|
||||||
|
if (!miner || !input.trim()) return;
|
||||||
|
|
||||||
|
this.minerService.sendStdin(miner, input).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.stdinInput.set('');
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Failed to send stdin:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isErrorLine(line: string): boolean {
|
isErrorLine(line: string): boolean {
|
||||||
const lower = line.toLowerCase();
|
const lower = line.toLowerCase();
|
||||||
return lower.includes('error') || lower.includes('failed') || lower.includes('fatal');
|
return lower.includes('error') || lower.includes('failed') || lower.includes('fatal');
|
||||||
|
|
@ -417,4 +496,65 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
||||||
const lower = line.toLowerCase();
|
const lower = line.toLowerCase();
|
||||||
return lower.includes('warn') || lower.includes('timeout') || lower.includes('retry');
|
return lower.includes('warn') || lower.includes('timeout') || lower.includes('retry');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert ANSI escape codes to HTML with CSS styling
|
||||||
|
ansiToHtml(text: string): SafeHtml {
|
||||||
|
// ANSI color codes mapping
|
||||||
|
const colors: { [key: string]: string } = {
|
||||||
|
'30': '#1e1e1e', '31': '#ef4444', '32': '#22c55e', '33': '#eab308',
|
||||||
|
'34': '#3b82f6', '35': '#a855f7', '36': '#06b6d4', '37': '#e5e5e5',
|
||||||
|
'90': '#737373', '91': '#fca5a5', '92': '#86efac', '93': '#fde047',
|
||||||
|
'94': '#93c5fd', '95': '#d8b4fe', '96': '#67e8f9', '97': '#ffffff',
|
||||||
|
};
|
||||||
|
const bgColors: { [key: string]: string } = {
|
||||||
|
'40': '#1e1e1e', '41': '#dc2626', '42': '#16a34a', '43': '#ca8a04',
|
||||||
|
'44': '#2563eb', '45': '#9333ea', '46': '#0891b2', '47': '#d4d4d4',
|
||||||
|
};
|
||||||
|
|
||||||
|
let html = this.escapeHtml(text);
|
||||||
|
let currentStyles: string[] = [];
|
||||||
|
|
||||||
|
// Process ANSI escape sequences
|
||||||
|
html = html.replace(/\x1b\[([0-9;]*)m/g, (_, codes) => {
|
||||||
|
if (!codes || codes === '0') {
|
||||||
|
currentStyles = [];
|
||||||
|
return '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeList = codes.split(';');
|
||||||
|
const styles: string[] = [];
|
||||||
|
|
||||||
|
for (const code of codeList) {
|
||||||
|
if (code === '1') styles.push('font-weight:bold');
|
||||||
|
else if (code === '3') styles.push('font-style:italic');
|
||||||
|
else if (code === '4') styles.push('text-decoration:underline');
|
||||||
|
else if (colors[code]) styles.push(`color:${colors[code]}`);
|
||||||
|
else if (bgColors[code]) styles.push(`background:${bgColors[code]};padding:0 2px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (styles.length > 0) {
|
||||||
|
currentStyles = styles;
|
||||||
|
return `<span style="${styles.join(';')}">`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up any unclosed spans
|
||||||
|
const openSpans = (html.match(/<span/g) || []).length;
|
||||||
|
const closeSpans = (html.match(/<\/span>/g) || []).length;
|
||||||
|
for (let i = 0; i < openSpans - closeSpans; i++) {
|
||||||
|
html += '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sanitizer.bypassSecurityTrustHtml(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private escapeHtml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||