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"`
|
||||
mu sync.RWMutex
|
||||
cmd *exec.Cmd
|
||||
stdinPipe io.WriteCloser `json:"-"`
|
||||
HashrateHistory []HashratePoint `json:"hashrateHistory"`
|
||||
LowResHashrateHistory []HashratePoint `json:"lowResHashrateHistory"`
|
||||
LastLowResAggregation time.Time `json:"-"`
|
||||
|
|
@ -135,9 +136,33 @@ func (b *BaseMiner) Stop() error {
|
|||
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()
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (b *BaseMiner) Uninstall() error {
|
||||
return os.RemoveAll(b.GetPath())
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type Miner interface {
|
|||
AddHashratePoint(point HashratePoint)
|
||||
ReduceHashrateHistory(now time.Time)
|
||||
GetLogs() []string
|
||||
WriteStdin(input string) error
|
||||
}
|
||||
|
||||
// InstallationDetails contains information about an installed miner.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package mining
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
|
@ -129,6 +130,7 @@ func (s *Service) SetupRoutes() {
|
|||
minersGroup.GET("/:miner_name/stats", s.handleGetMinerStats)
|
||||
minersGroup.GET("/:miner_name/hashrate-history", s.handleGetMinerHashrateHistory)
|
||||
minersGroup.GET("/:miner_name/logs", s.handleGetMinerLogs)
|
||||
minersGroup.POST("/:miner_name/stdin", s.handleMinerStdin)
|
||||
}
|
||||
|
||||
// Historical data endpoints (database-backed)
|
||||
|
|
@ -471,11 +473,11 @@ func (s *Service) handleGetMinerHashrateHistory(c *gin.Context) {
|
|||
|
||||
// handleGetMinerLogs godoc
|
||||
// @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
|
||||
// @Produce json
|
||||
// @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]
|
||||
func (s *Service) handleGetMinerLogs(c *gin.Context) {
|
||||
minerName := c.Param("miner_name")
|
||||
|
|
@ -485,7 +487,51 @@ func (s *Service) handleGetMinerLogs(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ func (m *TTMiner) Start(config *Config) error {
|
|||
|
||||
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
|
||||
if m.LogBuffer != nil {
|
||||
m.cmd.Stdout = m.LogBuffer
|
||||
|
|
|
|||
|
|
@ -62,6 +62,13 @@ func (m *XMRigMiner) Start(config *Config) error {
|
|||
|
||||
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
|
||||
if m.LogBuffer != nil {
|
||||
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 { MainLayoutComponent } from './layouts/main-layout.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';
|
||||
import { SystemTrayComponent } from './pages/system-tray/system-tray.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
// System tray is standalone without layout
|
||||
{ path: 'system-tray', component: SystemTrayComponent },
|
||||
|
||||
// All other routes use the main layout
|
||||
{
|
||||
path: '',
|
||||
component: MainLayoutComponent,
|
||||
children: [
|
||||
{ path: '', redirectTo: 'workers', pathMatch: 'full' },
|
||||
{ path: 'workers', component: WorkersComponent },
|
||||
{ path: 'graphs', component: GraphsComponent },
|
||||
{ path: 'console', component: ConsoleComponent },
|
||||
{ path: 'pools', component: PoolsComponent },
|
||||
{ path: 'profiles', component: ProfilesComponent },
|
||||
{ path: 'miners', component: MinersComponent },
|
||||
]
|
||||
},
|
||||
// Main app routes - MainLayoutComponent is rendered directly and contains router-outlet
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
{ path: 'dashboard', component: GraphsComponent },
|
||||
{ path: 'workers', component: WorkersComponent },
|
||||
{ path: 'console', component: ConsoleComponent },
|
||||
{ path: 'pools', component: PoolsComponent },
|
||||
{ path: 'profiles', component: ProfilesComponent },
|
||||
{ path: 'miners', component: MinersComponent },
|
||||
{ path: 'nodes', component: NodesComponent },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { MainLayoutComponent } from './layouts/main-layout.component';
|
|||
imports: [
|
||||
CommonModule,
|
||||
SetupWizardComponent,
|
||||
MainLayoutComponent
|
||||
MainLayoutComponent,
|
||||
],
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ['./app.css'],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,55 @@
|
|||
.chart-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,17 @@
|
|||
<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
|
||||
class="hashrate-chart"
|
||||
[Highcharts]="Highcharts"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type SeriesWithData = Highcharts.SeriesAreaOptions | Highcharts.SeriesSplineOpti
|
|||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class ChartComponent {
|
||||
private minerService = inject(MinerService);
|
||||
minerService = inject(MinerService); // Public for template access
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
Highcharts: typeof Highcharts = Highcharts;
|
||||
|
|
@ -60,7 +60,7 @@ export class ChartComponent {
|
|||
...this.createBaseChartOptions().chart,
|
||||
type: 'area'
|
||||
},
|
||||
title: { text: 'Total Hashrate' },
|
||||
title: { text: '' },
|
||||
plotOptions: {
|
||||
area: {
|
||||
stacking: 'normal',
|
||||
|
|
@ -74,12 +74,8 @@ export class ChartComponent {
|
|||
|
||||
// Create effect with proper cleanup
|
||||
const effectRef = effect(() => {
|
||||
const historyMap = this.minerService.hashrateHistory();
|
||||
|
||||
// Skip if no data
|
||||
if (historyMap.size === 0) {
|
||||
return;
|
||||
}
|
||||
// Use 24-hour historical data from database
|
||||
const historyMap = this.minerService.historicalHashrate();
|
||||
|
||||
// Clean up colors for miners no longer active
|
||||
const activeNames = new Set(historyMap.keys());
|
||||
|
|
@ -107,7 +103,7 @@ export class ChartComponent {
|
|||
// Build new chart options
|
||||
this.chartOptions = {
|
||||
...this.createBaseChartOptions(),
|
||||
title: { text: 'Total Hashrate' },
|
||||
title: { text: '' },
|
||||
chart: {
|
||||
...this.createBaseChartOptions().chart,
|
||||
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 { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
icon: SafeHtml;
|
||||
route: string;
|
||||
}
|
||||
|
||||
|
|
@ -210,55 +211,61 @@ interface NavItem {
|
|||
`]
|
||||
})
|
||||
export class SidebarComponent {
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
collapsed = signal(false);
|
||||
currentRoute = input<string>('workers');
|
||||
currentRoute = input<string>('dashboard');
|
||||
routeChange = output<string>();
|
||||
|
||||
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',
|
||||
label: '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>'
|
||||
},
|
||||
{
|
||||
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>'
|
||||
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: 'console',
|
||||
label: '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',
|
||||
label: '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',
|
||||
label: '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',
|
||||
label: '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',
|
||||
label: '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() {
|
||||
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 { 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 { StatsPanelComponent } from '../components/stats-panel/stats-panel.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({
|
||||
selector: 'app-main-layout',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterOutlet,
|
||||
SidebarComponent,
|
||||
StatsPanelComponent,
|
||||
MinerSwitcherComponent,
|
||||
WorkersComponent,
|
||||
GraphsComponent,
|
||||
ConsoleComponent,
|
||||
PoolsComponent,
|
||||
ProfilesComponent,
|
||||
MinersComponent,
|
||||
NodesComponent
|
||||
],
|
||||
template: `
|
||||
<div class="main-layout">
|
||||
|
|
@ -38,32 +28,7 @@ import { NodesComponent } from '../pages/nodes/nodes.component';
|
|||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
@switch (currentRoute()) {
|
||||
@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>
|
||||
}
|
||||
}
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,17 +67,42 @@ import { NodesComponent } from '../pages/nodes/nodes.component';
|
|||
}
|
||||
`]
|
||||
})
|
||||
export class MainLayoutComponent {
|
||||
currentRoute = signal('workers');
|
||||
private editingProfileId: string | null = null;
|
||||
export class MainLayoutComponent implements AfterViewInit {
|
||||
private router = inject(Router);
|
||||
|
||||
// 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) {
|
||||
this.currentRoute.set(route);
|
||||
this.router.navigate(['/', route]);
|
||||
}
|
||||
|
||||
navigateToProfiles(profileId: string) {
|
||||
this.editingProfileId = profileId;
|
||||
this.currentRoute.set('profiles');
|
||||
// TODO: Could emit event to profiles page to open edit modal for this profile
|
||||
// TODO: Could pass profileId via query params or state
|
||||
this.router.navigate(['/', 'profiles']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,24 @@ export class MinerService implements OnDestroy {
|
|||
// Separate signal for hashrate history as it updates frequently
|
||||
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) ---
|
||||
public viewMode = signal<'all' | 'single'>('all');
|
||||
public selectedMinerName = signal<string | null>(null);
|
||||
|
|
@ -92,10 +110,12 @@ export class MinerService implements OnDestroy {
|
|||
constructor(private http: HttpClient) {
|
||||
this.forceRefreshState();
|
||||
this.startPollingLive_Data();
|
||||
this.startPollingHistoricalData();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopPolling();
|
||||
this.historyPollingSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
// --- Data Loading and Polling Logic ---
|
||||
|
|
@ -116,6 +136,8 @@ export class MinerService implements OnDestroy {
|
|||
if (initialState) {
|
||||
this.state.set(initialState);
|
||||
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() {
|
||||
this.pollingSubscription?.unsubscribe();
|
||||
}
|
||||
|
|
@ -185,7 +267,24 @@ export class MinerService implements OnDestroy {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, inject, computed, signal, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewChecked } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { MinerService } from '../../miner.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { interval, Subscription, switchMap } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
|
@ -58,7 +58,7 @@ import { interval, Subscription, switchMap } from 'rxjs';
|
|||
@if (logs().length > 0) {
|
||||
@for (line of logs(); track $index) {
|
||||
<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>
|
||||
}
|
||||
} @else if (activeMiner()) {
|
||||
|
|
@ -75,6 +75,19 @@ import { interval, Subscription, switchMap } from 'rxjs';
|
|||
}
|
||||
</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 -->
|
||||
<div class="console-controls">
|
||||
<label class="control-checkbox">
|
||||
|
|
@ -259,6 +272,49 @@ import { interval, Subscription, switchMap } from 'rxjs';
|
|||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -314,7 +370,7 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|||
@ViewChild('consoleOutput') consoleOutput!: ElementRef;
|
||||
|
||||
private minerService = inject(MinerService);
|
||||
private http = inject(HttpClient);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
private state = this.minerService.state;
|
||||
private pollSub?: Subscription;
|
||||
|
||||
|
|
@ -337,13 +393,16 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|||
|
||||
logs = signal<string[]>([]);
|
||||
autoScroll = signal(true);
|
||||
stdinInput = signal('');
|
||||
private shouldScroll = false;
|
||||
|
||||
ngOnInit() {
|
||||
// Auto-select first miner for console view
|
||||
// Auto-select first miner for console view and fetch logs immediately
|
||||
const miners = this.runningMiners();
|
||||
if (miners.length > 0) {
|
||||
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
|
||||
|
|
@ -351,12 +410,12 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|||
switchMap(() => {
|
||||
const miner = this.activeMiner();
|
||||
if (!miner) return [];
|
||||
return this.http.get<{logs: string[]}>(`/api/v1/mining/miners/${miner}/logs`);
|
||||
return this.minerService.getMinerLogs(miner);
|
||||
})
|
||||
).subscribe({
|
||||
next: (response: any) => {
|
||||
if (response?.logs) {
|
||||
this.logs.set(response.logs);
|
||||
next: (logs: string[]) => {
|
||||
if (logs && Array.isArray(logs)) {
|
||||
this.logs.set(logs);
|
||||
if (this.autoScroll()) {
|
||||
this.shouldScroll = true;
|
||||
}
|
||||
|
|
@ -385,10 +444,10 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|||
}
|
||||
|
||||
private fetchLogs(minerName: string) {
|
||||
this.http.get<{logs: string[]}>(`/api/v1/mining/miners/${minerName}/logs`).subscribe({
|
||||
next: (response) => {
|
||||
if (response?.logs) {
|
||||
this.logs.set(response.logs);
|
||||
this.minerService.getMinerLogs(minerName).subscribe({
|
||||
next: (logs) => {
|
||||
if (logs && Array.isArray(logs)) {
|
||||
this.logs.set(logs);
|
||||
this.shouldScroll = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -408,6 +467,26 @@ export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
|
|||
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 {
|
||||
const lower = line.toLowerCase();
|
||||
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();
|
||||
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, ''');
|
||||
}
|
||||
}
|
||||
|
|
|
|||