Skip to content

Commit 64a29b6

Browse files
committed
Mclogs upload widget implemented to call rpc
1 parent 408a875 commit 64a29b6

File tree

5 files changed

+195
-46
lines changed

5 files changed

+195
-46
lines changed

internal/rpc/services/server.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package services
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"fmt"
78
"io"
9+
"net/http"
810
"os"
911
"path/filepath"
1012
"slices"
@@ -1361,6 +1363,73 @@ func (s *ServerService) SendCommand(ctx context.Context, req *connect.Request[v1
13611363
}), nil
13621364
}
13631365

1366+
// Reads the server's latest.log and uploads it to mclo.gs
1367+
func (s *ServerService) UploadToMCLogs(ctx context.Context, req *connect.Request[v1.UploadToMCLogsRequest]) (*connect.Response[v1.UploadToMCLogsResponse], error) {
1368+
server, err := s.store.GetServer(ctx, req.Msg.Id)
1369+
if err != nil {
1370+
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("server not found"))
1371+
}
1372+
1373+
logPath := filepath.Join(server.DataPath, "logs", "latest.log")
1374+
content, err := os.ReadFile(logPath)
1375+
if err != nil {
1376+
s.log.Error("Failed to read server log file: %v", err)
1377+
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("log file not found"))
1378+
}
1379+
1380+
if len(content) == 0 {
1381+
return nil, connect.NewError(connect.CodeFailedPrecondition, fmt.Errorf("log file is empty"))
1382+
}
1383+
1384+
// Truncate to 25000 lines if needed
1385+
lines := bytes.Split(content, []byte("\n"))
1386+
if len(lines) > 25000 {
1387+
lines = lines[len(lines)-25000:]
1388+
content = bytes.Join(lines, []byte("\n"))
1389+
}
1390+
1391+
// Build mclo.gs request
1392+
payload, _ := json.Marshal(map[string]string{
1393+
"content": string(content),
1394+
"source": fmt.Sprintf("DiscoPanel-%s", server.Name),
1395+
})
1396+
1397+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.mclo.gs/1/log", bytes.NewReader(payload))
1398+
if err != nil {
1399+
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to create request"))
1400+
}
1401+
httpReq.Header.Set("Content-Type", "application/json")
1402+
1403+
resp, err := http.DefaultClient.Do(httpReq)
1404+
if err != nil {
1405+
s.log.Error("Failed to upload to mclo.gs: %v", err)
1406+
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to upload to mclo.gs"))
1407+
}
1408+
defer resp.Body.Close()
1409+
1410+
body, err := io.ReadAll(resp.Body)
1411+
if err != nil {
1412+
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to read mclo.gs response"))
1413+
}
1414+
1415+
var result struct {
1416+
Success bool `json:"success"`
1417+
URL string `json:"url"`
1418+
Error string `json:"error"`
1419+
}
1420+
if err := json.Unmarshal(body, &result); err != nil {
1421+
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to parse mclo.gs response"))
1422+
}
1423+
1424+
if !result.Success {
1425+
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("mclo.gs error: %s", result.Error))
1426+
}
1427+
1428+
return connect.NewResponse(&v1.UploadToMCLogsResponse{
1429+
Url: result.URL,
1430+
}), nil
1431+
}
1432+
13641433
// GetServerLogs gets server logs
13651434
func (s *ServerService) GetServerLogs(ctx context.Context, req *connect.Request[v1.GetServerLogsRequest]) (*connect.Response[v1.GetServerLogsResponse], error) {
13661435
// Parse tail parameter

proto/discopanel/v1/server.proto

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ service ServerService {
3535
rpc RecreateServer(RecreateServerRequest) returns (RecreateServerResponse);
3636
// Execute console command
3737
rpc SendCommand(SendCommandRequest) returns (SendCommandResponse);
38+
// Upload server logs to mclo.gs
39+
rpc UploadToMCLogs(UploadToMCLogsRequest) returns (UploadToMCLogsResponse);
3840
}
3941

4042
// Server list options
@@ -214,3 +216,13 @@ message SendCommandResponse {
214216
string output = 2;
215217
string error = 3;
216218
}
219+
220+
// Server to upload logs for
221+
message UploadToMCLogsRequest {
222+
string id = 1;
223+
}
224+
225+
// mclo.gs upload result
226+
message UploadToMCLogsResponse {
227+
string url = 1;
228+
}

web/discopanel/eslint.config.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,19 @@ export default ts.config(
2323
rules: {
2424
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
2525
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
26-
'no-undef': 'off'
26+
'no-undef': 'off',
27+
"@typescript-eslint/no-unused-vars": [
28+
"error",
29+
{
30+
"args": "all",
31+
"argsIgnorePattern": "^_",
32+
"caughtErrors": "all",
33+
"caughtErrorsIgnorePattern": "^_",
34+
"destructuredArrayIgnorePattern": "^_",
35+
"varsIgnorePattern": "^_",
36+
"ignoreRestSiblings": true
37+
}
38+
]
2739
}
2840
},
2941
{

web/discopanel/src/lib/components/server-console.svelte

Lines changed: 86 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
import type { Server } from '$lib/proto/discopanel/v1/common_pb';
66
import { ServerStatus } from '$lib/proto/discopanel/v1/common_pb';
77
import type { LogEntry } from '$lib/proto/discopanel/v1/server_pb';
8-
import { GetServerLogsRequestSchema, ClearServerLogsRequestSchema, SendCommandRequestSchema } from '$lib/proto/discopanel/v1/server_pb';
8+
import { GetServerLogsRequestSchema, ClearServerLogsRequestSchema, SendCommandRequestSchema, UploadToMCLogsRequestSchema } from '$lib/proto/discopanel/v1/server_pb';
99
import { ResizablePaneGroup, ResizablePane, ResizableHandle } from '$lib/components/ui/resizable';
1010
import { Button } from '$lib/components/ui/button';
1111
import { Badge } from '$lib/components/ui/badge';
1212
import { toast } from 'svelte-sonner';
13-
import { Terminal, Send, Loader2, Download, Trash2, RefreshCw, Wifi, WifiOff } from '@lucide/svelte';
13+
import { Terminal, Send, Loader2, Download, Upload, Trash2, RefreshCw, Wifi, WifiOff } from '@lucide/svelte';
14+
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
1415
import AnsiToHtml from 'ansi-to-html';
1516
import { getStringForEnum } from '$lib/utils';
1617
import { wsClient } from '$lib/stores/websocket.svelte';
@@ -216,6 +217,23 @@
216217
toast.success('Console cleared');
217218
}
218219
220+
let uploading = $state(false);
221+
222+
async function uploadToMCLogs() {
223+
if (uploading) return;
224+
uploading = true;
225+
try {
226+
const request = create(UploadToMCLogsRequestSchema, { id: server.id });
227+
const response = await rpcClient.server.uploadToMCLogs(request);
228+
await navigator.clipboard.writeText(response.url);
229+
toast.success('mclo.gs URL copied to clipboard');
230+
} catch (error) {
231+
toast.error('Failed to upload to mclo.gs: ' + (error instanceof Error ? error.message : 'Unknown error'));
232+
} finally {
233+
uploading = false;
234+
}
235+
}
236+
219237
function downloadLogs() {
220238
const logText = logEntries.map(entry => entry.message).join('\n');
221239
const blob = new Blob([logText], { type: 'text/plain' });
@@ -270,37 +288,70 @@
270288
{/if}
271289
</div>
272290
<div class="flex items-center gap-1">
273-
<Button
274-
size="sm"
275-
variant="ghost"
276-
onclick={fetchLogs}
277-
disabled={loading}
278-
class="h-7 w-7 p-0 text-zinc-400 hover:text-white"
279-
>
280-
{#if loading}
281-
<Loader2 class="h-3 w-3 animate-spin" />
282-
{:else}
283-
<RefreshCw class="h-3 w-3" />
284-
{/if}
285-
</Button>
286-
<Button
287-
size="sm"
288-
variant="ghost"
289-
onclick={downloadLogs}
290-
disabled={logEntries.length === 0}
291-
class="h-7 w-7 p-0 text-zinc-400 hover:text-white"
292-
>
293-
<Download class="h-3 w-3" />
294-
</Button>
295-
<Button
296-
size="sm"
297-
variant="ghost"
298-
onclick={clearLogs}
299-
disabled={logEntries.length === 0}
300-
class="h-7 w-7 p-0 text-zinc-400 hover:text-white"
301-
>
302-
<Trash2 class="h-3 w-3" />
303-
</Button>
291+
<Tooltip.Root>
292+
<Tooltip.Trigger>
293+
<Button
294+
size="sm"
295+
variant="ghost"
296+
onclick={fetchLogs}
297+
disabled={loading}
298+
class="h-7 w-7 p-0 text-zinc-400 hover:text-white"
299+
>
300+
{#if loading}
301+
<Loader2 class="h-3 w-3 animate-spin" />
302+
{:else}
303+
<RefreshCw class="h-3 w-3" />
304+
{/if}
305+
</Button>
306+
</Tooltip.Trigger>
307+
<Tooltip.Content>Refresh logs</Tooltip.Content>
308+
</Tooltip.Root>
309+
<Tooltip.Root>
310+
<Tooltip.Trigger>
311+
<Button
312+
size="sm"
313+
variant="ghost"
314+
onclick={uploadToMCLogs}
315+
disabled={uploading}
316+
class="h-7 w-7 p-0 text-zinc-400 hover:text-white"
317+
>
318+
{#if uploading}
319+
<Loader2 class="h-3 w-3 animate-spin" />
320+
{:else}
321+
<Upload class="h-3 w-3" />
322+
{/if}
323+
</Button>
324+
</Tooltip.Trigger>
325+
<Tooltip.Content>Upload to mclo.gs</Tooltip.Content>
326+
</Tooltip.Root>
327+
<Tooltip.Root>
328+
<Tooltip.Trigger>
329+
<Button
330+
size="sm"
331+
variant="ghost"
332+
onclick={downloadLogs}
333+
disabled={logEntries.length === 0}
334+
class="h-7 w-7 p-0 text-zinc-400 hover:text-white"
335+
>
336+
<Download class="h-3 w-3" />
337+
</Button>
338+
</Tooltip.Trigger>
339+
<Tooltip.Content>Download logs</Tooltip.Content>
340+
</Tooltip.Root>
341+
<Tooltip.Root>
342+
<Tooltip.Trigger>
343+
<Button
344+
size="sm"
345+
variant="ghost"
346+
onclick={clearLogs}
347+
disabled={logEntries.length === 0}
348+
class="h-7 w-7 p-0 text-zinc-400 hover:text-white"
349+
>
350+
<Trash2 class="h-3 w-3" />
351+
</Button>
352+
</Tooltip.Trigger>
353+
<Tooltip.Content>Clear console</Tooltip.Content>
354+
</Tooltip.Root>
304355
</div>
305356
</div>
306357
<div
@@ -314,8 +365,9 @@
314365
No logs available. {[ServerStatus.RUNNING, ServerStatus.STARTING, ServerStatus.UNHEALTHY].includes(server.status) ? 'Try refreshing the page.' : 'Start the server to see output.'}
315366
</div>
316367
{:else}
317-
{#each logEntries as entry}
368+
{#each logEntries as entry, i (i)}
318369
<div class="log-line whitespace-pre-wrap break-all" data-type={entry.level}>
370+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
319371
{@html ansiConverter.toHtml(entry.message)}
320372
</div>
321373
{/each}

web/discopanel/src/routes/servers/[id]/+page.svelte

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
serversStore.updateServer(server);
8484
loading = false;
8585
}
86-
} catch (error) {
86+
} catch {
8787
// Only show error if still on the same server and no data yet
8888
if (serverId === requestedId && !server) {
8989
toast.error('Failed to load server');
@@ -98,26 +98,30 @@
9898
actionLoading = true;
9999
try {
100100
switch (action) {
101-
case 'start':
101+
case 'start': {
102102
const startRequest = create(StartServerRequestSchema, { id: server.id });
103103
await rpcClient.server.startServer(startRequest);
104104
toast.success('Server is starting...');
105105
break;
106-
case 'stop':
106+
}
107+
case 'stop': {
107108
const stopRequest = create(StopServerRequestSchema, { id: server.id });
108109
await rpcClient.server.stopServer(stopRequest);
109110
toast.success('Server is stopping...');
110111
break;
111-
case 'restart':
112+
}
113+
case 'restart': {
112114
const restartRequest = create(RestartServerRequestSchema, { id: server.id });
113115
await rpcClient.server.restartServer(restartRequest);
114116
toast.success('Server is restarting...');
115117
break;
116-
case 'recreate':
118+
}
119+
case 'recreate': {
117120
const recreateRequest = create(RecreateServerRequestSchema, { id: server.id });
118121
await rpcClient.server.recreateServer(recreateRequest);
119122
toast.success('Server is being recreated...');
120123
break;
124+
}
121125
}
122126
await loadServer();
123127
} catch (error) {
@@ -382,33 +386,33 @@
382386
<div class="flex items-center justify-center h-20 rounded-xl bg-gradient-to-br from-muted/30 to-muted/10 border border-border/30 overflow-hidden">
383387
{#if server.status === ServerStatus.RUNNING}
384388
<div class="heartbeat-container">
385-
{#each Array(5) as _, i}
389+
{#each Array(5) as _, i (i)}
386390
<div class="heartbeat-bar bg-green-500" style="animation-delay: {i * 0.15}s"></div>
387391
{/each}
388392
</div>
389393
{:else if server.status === ServerStatus.UNHEALTHY}
390394
<div class="heartbeat-container">
391-
{#each Array(5) as _, i}
395+
{#each Array(5) as _, i (i)}
392396
<div class="heartbeat-bar heartbeat-erratic text-purple-500" style="animation-delay: {i * 0.1}s; height: {20 + Math.random() * 30}px"></div>
393397
{/each}
394398
</div>
395399
{:else if server.status === ServerStatus.STOPPED}
396400
<div class="w-full h-0.5 bg-gray-500/50"></div>
397401
{:else if server.status === ServerStatus.STARTING}
398402
<div class="heartbeat-container">
399-
{#each Array(5) as _, i}
403+
{#each Array(5) as _, i (i)}
400404
<div class="heartbeat-bar heartbeat-slow bg-yellow-500" style="animation-delay: {i * 0.2}s"></div>
401405
{/each}
402406
</div>
403407
{:else if server.status === ServerStatus.CREATING}
404408
<div class="heartbeat-container">
405-
{#each Array(5) as _, i}
409+
{#each Array(5) as _, i (i)}
406410
<div class="heartbeat-bar heartbeat-slow bg-blue-500" style="animation-delay: {i * 0.2}s"></div>
407411
{/each}
408412
</div>
409413
{:else}
410414
<div class="heartbeat-container">
411-
{#each Array(5) as _, i}
415+
{#each Array(5) as _, i (i)}
412416
<div class="heartbeat-bar heartbeat-slow bg-orange-500" style="animation-delay: {i * 0.25}s"></div>
413417
{/each}
414418
</div>
@@ -595,7 +599,7 @@
595599
</div>
596600
{#if server.playerSample && server.playerSample.length > 0}
597601
<div class="flex flex-wrap gap-1.5">
598-
{#each server.playerSample as playerName}
602+
{#each server.playerSample as playerName (playerName)}
599603
<div class="flex items-center gap-1 px-1.5 py-0.5 rounded border transition-colors duration-500"
600604
style="background: rgb({colors.bg} / 0.1); border-color: rgb({colors.bg} / 0.2);">
601605
<img

0 commit comments

Comments
 (0)