diff --git a/app.go b/app.go
index 8d4da2b..29be1a4 100644
--- a/app.go
+++ b/app.go
@@ -1,7 +1,14 @@
package main
import (
+ "bytes"
"context"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+ nethttp "net/http"
+ "os"
"runtime/debug"
"strconv"
@@ -21,10 +28,12 @@ import (
// App struct
type App struct {
- ctx context.Context
- cliParams *cli.Context
- driver database.DatabaseDriver
- ready *sync.RWSync[bool]
+ ctx context.Context
+ cliParams *cli.Context
+ driver database.DatabaseDriver
+ ready *sync.RWSync[bool]
+ isFatalError *sync.RWSync[bool]
+ backupOptions database.BackupOptions
db *database.LocalDBClient
twitchManager *twitch.Manager
@@ -35,50 +44,79 @@ type App struct {
// NewApp creates a new App application struct
func NewApp(cliParams *cli.Context) *App {
return &App{
- cliParams: cliParams,
- ready: sync.NewRWSync(false),
+ cliParams: cliParams,
+ ready: sync.NewRWSync(false),
+ isFatalError: sync.NewRWSync(false),
}
}
// startup is called when the app starts
func (a *App) startup(ctx context.Context) {
+ defer func() {
+ if r := recover(); r != nil {
+ a.stop(ctx)
+ _ = logger.Sync()
+ switch v := r.(type) {
+ case error:
+ a.showFatalError(v, v.Error())
+ default:
+ a.showFatalError(errors.New(fmt.Sprint(v)), "Runtime error encountered")
+ }
+ }
+ }()
+
a.ctx = ctx
-
- // Make KV hub
- var err error
- a.driver, err = database.GetDatabaseDriver(a.cliParams)
- failOnError(err, "error opening database")
-
- // Start database backup task
- backupOpts := database.BackupOptions{
+ a.backupOptions = database.BackupOptions{
BackupDir: a.cliParams.String("backup-dir"),
BackupInterval: a.cliParams.Int("backup-interval"),
MaxBackups: a.cliParams.Int("max-backups"),
}
- if backupOpts.BackupInterval > 0 {
- go BackupTask(a.driver, backupOpts)
+
+ // Make KV hub
+ var err error
+ a.driver, err = database.GetDatabaseDriver(a.cliParams)
+ if err != nil {
+ a.showFatalError(err, "Error opening database")
+ return
+ }
+
+ // Start database backup task
+ if a.backupOptions.BackupInterval > 0 {
+ go BackupTask(a.driver, a.backupOptions)
}
hub := a.driver.Hub()
go hub.Run()
a.db, err = database.NewLocalClient(hub, logger)
- failOnError(err, "failed to initialize database module")
+ if err != nil {
+ a.showFatalError(err, "Failed to initialize database module")
+ return
+ }
// Set meta keys
_ = a.db.PutKey("stul-meta/version", appVersion)
// Create logger and endpoints
a.httpServer, err = http.NewServer(a.db, logger)
- failOnError(err, "could not initialize http server")
+ if err != nil {
+ a.showFatalError(err, "Could not initialize http server")
+ return
+ }
// Create twitch client
a.twitchManager, err = twitch.NewManager(a.db, a.httpServer, logger)
- failOnError(err, "could not initialize twitch client")
+ if err != nil {
+ a.showFatalError(err, "Could not initialize twitch client")
+ return
+ }
// Initialize loyalty system
a.loyaltyManager, err = loyalty.NewManager(a.db, a.twitchManager, logger)
- failOnError(err, "could not initialize loyalty manager")
+ if err != nil {
+ a.showFatalError(err, "Could not initialize loyalty manager")
+ return
+ }
a.ready.Set(true)
runtime.EventsEmit(ctx, "ready", true)
@@ -92,7 +130,10 @@ func (a *App) startup(ctx context.Context) {
}()
// Run HTTP server
- failOnError(a.httpServer.Listen(), "HTTP server stopped")
+ if err := a.httpServer.Listen(); err != nil {
+ a.showFatalError(err, "HTTP server stopped")
+ return
+ }
}
func (a *App) stop(context.Context) {
@@ -122,6 +163,10 @@ func (a *App) IsServerReady() bool {
return a.ready.Get()
}
+func (a *App) IsFatalError() bool {
+ return a.isFatalError.Get()
+}
+
func (a *App) GetKilovoltBind() string {
if a.httpServer == nil {
return ""
@@ -145,6 +190,78 @@ func (a *App) GetDocumentation() map[string]docs.KeyObject {
return docs.Keys
}
+func (a *App) SendCrashReport(errorData string, info string) (string, error) {
+ var b bytes.Buffer
+ w := multipart.NewWriter(&b)
+
+ // Add text fields
+ if err := w.WriteField("error", errorData); err != nil {
+ logger.Error("could not encode field error for crash report", zap.Error(err))
+ }
+ if len(info) > 0 {
+ if err := w.WriteField("info", info); err != nil {
+ logger.Error("could not encode field info for crash report", zap.Error(err))
+ }
+ }
+
+ // Add log files
+ _ = logger.Sync()
+ addFile(w, "log", logFilename)
+ addFile(w, "paniclog", panicFilename)
+
+ if err := w.Close(); err != nil {
+ logger.Error("could not prepare request for crash report", zap.Error(err))
+ return "", err
+ }
+
+ resp, err := nethttp.Post(crashReportURL, w.FormDataContentType(), &b)
+ if err != nil {
+ logger.Error("could not send crash report", zap.Error(err))
+ return "", err
+ }
+
+ // Check the response
+ if resp.StatusCode != nethttp.StatusOK {
+ byt, _ := io.ReadAll(resp.Body)
+ logger.Error("crash report server returned error", zap.String("status", resp.Status), zap.String("response", string(byt)))
+ return "", fmt.Errorf("crash report server returned error: %s - %s", resp.Status, string(byt))
+ }
+
+ byt, err := io.ReadAll(resp.Body)
+ return string(byt), err
+}
+
+type BackupInfo struct {
+ Filename string `json:"filename"`
+ Date int64 `json:"date"`
+}
+
+func (a *App) GetBackups() (list []BackupInfo) {
+ files, err := os.ReadDir(a.backupOptions.BackupDir)
+ if err != nil {
+ logger.Error("could not read backup directory", zap.Error(err))
+ return nil
+ }
+
+ for _, file := range files {
+ if file.IsDir() {
+ continue
+ }
+
+ info, err := file.Info()
+ if err != nil {
+ logger.Error("could not get info for backup file", zap.Error(err))
+ continue
+ }
+
+ list = append(list, BackupInfo{
+ Filename: file.Name(),
+ Date: info.ModTime().UnixMilli(),
+ })
+ }
+ return
+}
+
type VersionInfo struct {
Release string `json:"release"`
BuildInfo *debug.BuildInfo `json:"build"`
@@ -157,3 +274,31 @@ func (a *App) GetAppVersion() VersionInfo {
BuildInfo: info,
}
}
+
+func (a *App) showFatalError(err error, text string, fields ...zap.Field) {
+ if err != nil {
+ fields = append(fields, zap.Error(err))
+ fields = append(fields, zap.String("Z", string(debug.Stack())))
+ logger.Error(text, fields...)
+ runtime.EventsEmit(a.ctx, "fatalError")
+ a.isFatalError.Set(true)
+ }
+}
+
+func addFile(m *multipart.Writer, field string, filename string) {
+ logfile, err := m.CreateFormFile(field, filename)
+ if err != nil {
+ logger.Error("could not encode field log for crash report", zap.Error(err))
+ return
+ }
+
+ file, err := os.Open(filename)
+ if err != nil {
+ logger.Error("could not open file for including in crash report", zap.Error(err), zap.String("file", filename))
+ return
+ }
+
+ if _, err = io.Copy(logfile, file); err != nil {
+ logger.Error("could not read from file for including in crash report", zap.Error(err), zap.String("file", filename))
+ }
+}
diff --git a/database/driver.pebble.go b/database/driver.pebble.go
index 0f62b00..3c76e48 100644
--- a/database/driver.pebble.go
+++ b/database/driver.pebble.go
@@ -50,6 +50,9 @@ func (p *PebbleDatabase) Hub() *kv.Hub {
}
func (p *PebbleDatabase) Close() error {
+ if p.hub != nil {
+ p.hub.Close()
+ }
err := p.db.Close()
if err != nil {
return fmt.Errorf("could not close database: %w", err)
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index d09bd7d..626a02e 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -1,6 +1,6 @@
module.exports = {
parser: '@typescript-eslint/parser',
- plugins: ['@typescript-eslint', 'import'],
+ plugins: ['react-refresh', '@typescript-eslint', 'import'],
extends: [
'airbnb-base',
'plugin:@typescript-eslint/recommended',
@@ -28,6 +28,7 @@ module.exports = {
'@typescript-eslint/no-unsafe-return': ['error'],
'@typescript-eslint/switch-exhaustiveness-check': ['error'],
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
+ 'react-refresh/only-export-components': 'warn'
},
settings: {
'import/resolver': {
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 601e930..56632bc 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -55,6 +55,7 @@
"eslint-import-resolver-typescript": "^3.5.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-react-refresh": "^0.3.4",
"prettier": "^2.8.7",
"rimraf": "^4.4.1"
}
@@ -2818,6 +2819,15 @@
}
}
},
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.4.tgz",
+ "integrity": "sha512-E0ViBglxSQAERBp6eTj5fPgtCRtDonnbCFiVQBhf4Dto2blJRxg1dFUMdMh7N6ljTI4UwPhHwYDQ3Dyo4m6bwA==",
+ "dev": true,
+ "peerDependencies": {
+ "eslint": ">=7"
+ }
+ },
"node_modules/eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -7263,6 +7273,13 @@
"prettier-linter-helpers": "^1.0.0"
}
},
+ "eslint-plugin-react-refresh": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.4.tgz",
+ "integrity": "sha512-E0ViBglxSQAERBp6eTj5fPgtCRtDonnbCFiVQBhf4Dto2blJRxg1dFUMdMh7N6ljTI4UwPhHwYDQ3Dyo4m6bwA==",
+ "dev": true,
+ "requires": {}
+ },
"eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 320c392..5fceeda 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -58,6 +58,7 @@
"eslint-import-resolver-typescript": "^3.5.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-react-refresh": "^0.3.4",
"prettier": "^2.8.7",
"rimraf": "^4.4.1"
}
diff --git a/frontend/package.json.md5 b/frontend/package.json.md5
index 33a3a08..96f9a4c 100644
--- a/frontend/package.json.md5
+++ b/frontend/package.json.md5
@@ -1 +1 @@
-dc4f2d866ac751f1391a998d03e939db
\ No newline at end of file
+293b48c0e126b4f88d9b5ba934df5908
\ No newline at end of file
diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx
index b235653..75becb8 100644
--- a/frontend/src/index.tsx
+++ b/frontend/src/index.tsx
@@ -1,27 +1,47 @@
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { HashRouter } from 'react-router-dom';
-import { StrictMode } from 'react';
+import { StrictMode, useEffect, useState } from 'react';
+import { EventsOff, EventsOn } from '@wailsapp/runtime';
+import { IsFatalError } from '@wailsapp/go/main/App';
import 'inter-ui/inter.css';
import '@fontsource/space-mono/index.css';
import 'normalize.css/normalize.css';
-
import './locale/setup';
import store from './store';
import App from './ui/App';
+import ErrorWindow from './ui/ErrorWindow';
import { globalStyles } from './ui/theme';
globalStyles();
+function AppWrapper() {
+ const [fatalErrorEncountered, setFatalErrorStatus] = useState(false);
+ useEffect(() => {
+ void IsFatalError().then(setFatalErrorStatus);
+ EventsOn('fatalError', () => {
+ setFatalErrorStatus(true);
+ });
+ return () => {
+ EventsOff('fatalError');
+ };
+ }, []);
+
+ if (fatalErrorEncountered) {
+ return ;
+ }
+ return ;
+}
+
const main = document.getElementById('main');
const root = createRoot(main);
root.render(
-
+
,
diff --git a/frontend/src/locale/en/translation.json b/frontend/src/locale/en/translation.json
index 6797ff6..0537fc1 100644
--- a/frontend/src/locale/en/translation.json
+++ b/frontend/src/locale/en/translation.json
@@ -339,6 +339,31 @@
"error-alert": "Error details for {{name}}",
"incompatible-body": "This extension requires {{APPNAME}} version {{version}} and up, you are currently running {{appversion}}, which may be too old and miss required features",
"incompatible-warning": "This extension is not compatible"
+ },
+ "crash": {
+ "fatal-message": "A fatal error has occurred and strimertül has stopped working, check the details below:",
+ "action-header": "What to do?",
+ "action-submit-line": "Consider submitting this report using the Report button below so someone can look at it.",
+ "action-recover-line": "If this error happens every time you start the app or if you've been instructed to, click the Recovery button to restore the database to an earlier backup.",
+ "action-log-line": "Logs and other crash related info can be found in the log files {{A}} and {{B}}.",
+ "button-report": "Report this error",
+ "button-recovery": "Recovery options",
+ "app-log-header": "Application logs for this run",
+ "report": {
+ "button-send": "Send report",
+ "dialog-title": "Report this error",
+ "thanks-line": "Thanks for choosing to submit this error! If you want, please write below what you were trying to do or anything else that you think might help.",
+ "additional-label": "Additional info (optional)",
+ "text-placeholder": "What were you doing before this happened?",
+ "email-label": "Include email address (if you want to be contacted about this)",
+ "email-placeholder": "Write your email address here",
+ "transparency-line": "When clicking \"Send report\", the following info will be collected and sent:",
+ "transparency-files": "The contents of {{A}} and {{B}}",
+ "transparency-info": "Information about the error that triggered this crash",
+ "transparency-user": "The additional info below, if any was provided",
+ "error-message": "The crash report could not be submitted because of a remote error: {{error}}",
+ "post-report": "The error was successfully reported and has been assigned the following code: {{code}} If you haven't provided an email and want to follow up on this, use that code when opening an issue or reaching out."
+ }
}
},
"form-actions": {
diff --git a/frontend/src/locale/it/translation.json b/frontend/src/locale/it/translation.json
index dc52351..7c898a2 100644
--- a/frontend/src/locale/it/translation.json
+++ b/frontend/src/locale/it/translation.json
@@ -392,6 +392,29 @@
"error-alert": "Dettagli errore per {{name}}",
"incompatible-body": "Questa estensione richiede {{APPNAME}} versione {{version}} o successive, al momento stai utilizzando {{appversion}} che potrebbe essere troppo vecchia e perciò senza alcune funzionalità richieste",
"incompatible-warning": "Questa estensione non è compatibile"
+ },
+ "crash": {
+ "action-header": "Che fare?",
+ "action-recover-line": "Se questo errore si verifica ogni volta che avvii l'app o se ti è stato richiesto di farlo, fai clic sul pulsante Ripristino per recuperare il database da un backup precedente.",
+ "action-log-line": "I log e altre informazioni relative agli arresti anomali sono disponibili nei file di log {{A}} e {{B}}.",
+ "action-submit-line": "Puoi inviare una segnalazione di questo arresto anomalo utilizzando il pulsante Segnala in basso in modo che qualcuno possa esaminarla.",
+ "app-log-header": "Log applicazione per questa esecuzione",
+ "button-recovery": "Menù ripristino",
+ "button-report": "Segnala questo errore",
+ "fatal-message": "Si è verificato un errore irreversibile e strimertül ha smesso di funzionare, ecco i dettagli:",
+ "report": {
+ "additional-label": "Info aggiuntive (opzionale)",
+ "button-send": "Invia segnalazione",
+ "dialog-title": "Segnala questo errore",
+ "text-placeholder": "Cosa stavi facendo nell'applicazione?",
+ "thanks-line": "Grazie per aver scelto di segnalare questo errore! \nSe vuoi, scrivi qui sotto cosa stavi cercando di fare o qualsiasi altra cosa che pensi possa essere d'aiuto.",
+ "email-label": "Includi una email (se vuoi essere contattato in merito)",
+ "email-placeholder": "Scrivi qui il tuo indirizzo email",
+ "transparency-files": "I contenuti di {{A}} e {{B}}",
+ "transparency-info": "Informazioni sull'errore specifico che ha causato il crash",
+ "transparency-line": "Facendo clic su \"Invia segnalazione\", verranno raccolte e inviate i seguenti dati:",
+ "transparency-user": "Le informazioni aggiuntive di seguito, se fornite"
+ }
}
},
"time": {
diff --git a/frontend/src/store/logging/reducer.ts b/frontend/src/store/logging/reducer.ts
index 687d3f4..8d20eeb 100644
--- a/frontend/src/store/logging/reducer.ts
+++ b/frontend/src/store/logging/reducer.ts
@@ -10,7 +10,7 @@ export interface ProcessedLogEntry {
data: object;
}
-function processEntry({
+export function processEntry({
time,
caller,
level,
diff --git a/frontend/src/ui/App.tsx b/frontend/src/ui/App.tsx
index 8fcee2d..d877840 100644
--- a/frontend/src/ui/App.tsx
+++ b/frontend/src/ui/App.tsx
@@ -11,7 +11,7 @@ import {
} from '@radix-ui/react-icons';
import { EventsOff, EventsOn } from '@wailsapp/runtime/runtime';
import { useTranslation } from 'react-i18next';
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import {
diff --git a/frontend/src/ui/ErrorWindow.tsx b/frontend/src/ui/ErrorWindow.tsx
new file mode 100644
index 0000000..7331097
--- /dev/null
+++ b/frontend/src/ui/ErrorWindow.tsx
@@ -0,0 +1,474 @@
+import { CheckIcon } from '@radix-ui/react-icons';
+import {
+ GetBackups,
+ GetLastLogs,
+ SendCrashReport,
+} from '@wailsapp/go/main/App';
+import type { main } from '@wailsapp/go/models';
+import { EventsOff, EventsOn } from '@wailsapp/runtime';
+import { Fragment, useEffect, useState } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+import { languages } from '~/locale/languages';
+import { ProcessedLogEntry, processEntry } from '~/store/logging/reducer';
+import DialogContent from '~/ui/components/DialogContent';
+import { LogItem } from '~/ui/components/LogViewer';
+import Scrollbar from '~/ui/components/utils/Scrollbar';
+import {
+ Button,
+ Checkbox,
+ CheckboxIndicator,
+ Dialog,
+ DialogActions,
+ Field,
+ FlexRow,
+ InputBox,
+ Label,
+ MultiToggle,
+ MultiToggleItem,
+ PageContainer,
+ PageHeader,
+ SectionHeader,
+ styled,
+ Textarea,
+ TextBlock,
+} from '~/ui/theme';
+import AlertContent from './components/AlertContent';
+import { Alert, AlertDescription } from './theme/alert';
+
+const Container = styled('div', {
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'row',
+ overflow: 'hidden',
+ height: '100vh',
+ border: '2px solid $red10',
+});
+
+const ErrorHeader = styled('h1', {
+ color: '$red10',
+ textTransform: 'capitalize',
+});
+
+const ErrorDetails = styled('dl', {
+ display: 'grid',
+ gridTemplateColumns: '100px 1fr',
+ margin: '0',
+});
+const ErrorDetailKey = styled('dt', {
+ fontWeight: 'bold',
+ textTransform: 'capitalize',
+ gridColumn: '1',
+});
+const ErrorDetailValue = styled('dd', {
+ padding: '0',
+ margin: '0',
+ marginBottom: '0.5rem',
+ gridColumn: '2',
+});
+
+const LogContainer = styled('div', {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '3px',
+});
+
+const Mono = styled('code', {
+ background: '$gray5',
+ padding: '3px 5px',
+ borderRadius: '3px',
+ whiteSpace: 'nowrap',
+});
+
+const MiniHeader = styled(SectionHeader, {
+ fontSize: '14pt',
+});
+
+const LanguageSelector = styled('div', {
+ top: '10px',
+ right: '10px',
+ display: 'flex',
+ gap: '1rem',
+ position: 'absolute',
+ zIndex: '1',
+});
+
+const LanguageItem = styled(MultiToggleItem, {
+ fontSize: '8pt',
+ padding: '5px 6px 4px',
+ textTransform: 'uppercase',
+});
+
+const BackupItem = styled('article', {
+ marginBottom: '0.4rem',
+ backgroundColor: '$gray2',
+ margin: '0.5rem 0',
+ padding: '0.3rem 0.5rem',
+ borderLeft: '5px solid $teal8',
+ borderRadius: '0.25rem',
+ borderBottom: '1px solid $gray4',
+ transition: 'all 50ms',
+ display: 'flex',
+});
+
+const BackupDate = styled('div', {
+ flex: '1',
+ display: 'flex',
+ gap: '0.5rem',
+ alignItems: 'baseline',
+});
+const BackupActions = styled('div', {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '0.25rem',
+});
+
+interface RecoveryDialogProps {
+ open: boolean;
+ onOpenChange: (state: boolean) => void;
+}
+
+function RecoveryDialog({ open, onOpenChange }: RecoveryDialogProps) {
+ const { t } = useTranslation();
+ const [backups, setBackups] = useState([]);
+
+ useEffect(() => {
+ GetBackups().then((backupList) => {
+ setBackups(backupList);
+ console.log(backupList);
+ });
+ }, []);
+
+ return (
+
+ );
+}
+
+interface ReportDialogProps {
+ open: boolean;
+ onOpenChange: (state: boolean) => void;
+ errorData?: ProcessedLogEntry;
+}
+
+function ReportDialog({ open, onOpenChange, errorData }: ReportDialogProps) {
+ const { t } = useTranslation();
+ const [errorDesc, setErrorDesc] = useState('');
+ const [contactEnabled, setContactEnabled] = useState(false);
+ const [contactInfo, setContactInfo] = useState('');
+ const [submitted, setSubmitted] = useState(false);
+ const [code, setCode] = useState('');
+ const [submissionError, setSubmissionError] = useState(null);
+
+ const waiting = submitted && code.length < 1;
+
+ if (code) {
+ return (
+ {
+ if (onOpenChange) onOpenChange(state);
+ }}
+ >
+ {
+ setSubmissionError(null);
+ }}
+ >
+
+
+ ),
+ }}
+ />
+
+
+
+ );
+ }
+
+ return (
+ <>
+ {
+ if (!val) setSubmissionError(null);
+ }}
+ >
+ {
+ setSubmissionError(null);
+ }}
+ />
+
+
+ >
+ );
+}
+
+export default function ErrorWindow(): JSX.Element {
+ const [t, i18n] = useTranslation();
+ const [logs, setLogs] = useState([]);
+ const [reportDialogOpen, setReportDialogOpen] = useState(false);
+ const [recoveryDialogOpen, setRecoveryDialogOpen] = useState(false);
+
+ useEffect(() => {
+ void GetLastLogs().then((appLogs) => {
+ setLogs(appLogs.map(processEntry).reverse());
+ });
+ EventsOn('log-event', (event: main.LogEntry) => {
+ setLogs([processEntry(event), ...logs]);
+ });
+ return () => {
+ EventsOff('log-event');
+ };
+ }, []);
+
+ const fatal = logs.find((log) => log.level === 'error');
+
+ return (
+
+
+
+
+ {
+ void i18n.changeLanguage(newLang);
+ }}
+ >
+ {languages.map((lang) => (
+
+ {lang.code}
+
+ ))}
+
+
+
+
+
+ {t('pages.crash.fatal-message')}
+
+ {fatal ? (
+ <>
+ {fatal.message}
+
+ {Object.keys(fatal.data)
+ .filter((key) => key.length > 1)
+ .map((key) => (
+
+ {key}
+ {fatal.data[key]}
+
+ ))}
+
+ >
+ ) : null}
+ {t('pages.crash.action-header')}
+ {t('pages.crash.action-submit-line')}
+ {t('pages.crash.action-recover-line')}
+
+ ,
+ }}
+ />
+
+
+
+
+
+
+ {t('pages.crash.app-log-header')}
+
+ {logs.map((log) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/frontend/src/ui/components/LogViewer.tsx b/frontend/src/ui/components/LogViewer.tsx
index fdfe315..045a5fa 100644
--- a/frontend/src/ui/components/LogViewer.tsx
+++ b/frontend/src/ui/components/LogViewer.tsx
@@ -108,6 +108,7 @@ const LevelToggle = styled(MultiToggleItem, {
interface LogItemProps {
data: ProcessedLogEntry;
+ expandDefault?: boolean;
}
const LogEntryContainer = styled('div', {
@@ -231,12 +232,12 @@ const LogDetailKey = styled('div', {
});
const LogDetailValue = styled('div', { flex: '1' });
-function LogItem({ data }: LogItemProps) {
+export function LogItem({ data, expandDefault }: LogItemProps) {
const { t } = useTranslation();
const levelStyle = isSupportedLevel(data.level) ? data.level : null;
const details = Object.entries(data.data).filter(([key]) => key.length > 1);
const [copied, setCopied] = useState(false);
- const [showDetails, setShowDetails] = useState(false);
+ const [showDetails, setShowDetails] = useState(expandDefault ?? false);
const copyToClipboard = async () => {
await navigator.clipboard.writeText(JSON.stringify(data.data));
setCopied(true);
diff --git a/frontend/src/ui/components/utils/Channels.tsx b/frontend/src/ui/components/utils/Channels.tsx
new file mode 100644
index 0000000..16aa2e9
--- /dev/null
+++ b/frontend/src/ui/components/utils/Channels.tsx
@@ -0,0 +1,31 @@
+import {
+ DiscordLogoIcon,
+ EnvelopeClosedIcon,
+ GitHubLogoIcon,
+} from '@radix-ui/react-icons';
+import { ChannelList, Channel, ChannelLink } from '../../pages/Strimertul';
+
+export const Channels = (
+
+
+
+
+ github.com/strimertul/strimertul/issues
+
+
+
+
+
+ nebula.cafe/discord
+
+
+
+
+
+ strimertul@nebula.cafe
+
+
+
+);
+
+export default Channels;
diff --git a/frontend/src/ui/pages/Onboarding.tsx b/frontend/src/ui/pages/Onboarding.tsx
index b62bbb3..1549238 100644
--- a/frontend/src/ui/pages/Onboarding.tsx
+++ b/frontend/src/ui/pages/Onboarding.tsx
@@ -23,6 +23,7 @@ import AlertContent from '../components/AlertContent';
import BrowserLink from '../components/BrowserLink';
import DefinitionTable from '../components/DefinitionTable';
import RevealLink from '../components/utils/RevealLink';
+import Channels from '../components/utils/Channels';
import {
Button,
@@ -39,7 +40,6 @@ import {
TextBlock,
} from '../theme';
import { Alert } from '../theme/alert';
-import { channels } from './Strimertul';
const Container = styled('div', {
display: 'flex',
@@ -582,7 +582,7 @@ function DoneStep() {
{t('pages.onboarding.done-header')}
{t('pages.onboarding.done-p1')}
{t('pages.onboarding.done-p2')}
- {channels}
+ {Channels}
{t('pages.onboarding.done-p3')}