diff --git a/resources/i18n/pt.json b/resources/i18n/pt.json
index e37af877b..aced12a6a 100644
--- a/resources/i18n/pt.json
+++ b/resources/i18n/pt.json
@@ -291,7 +291,9 @@
"title": "Atividade",
"totalScanned": "Total de pastas encontradas",
"quickScan": "Scan rápido",
- "fullScan": "Scan completo"
+ "fullScan": "Scan completo",
+ "serverUptime": "Uptime do servidor",
+ "serverDown": "DESCONECTADO"
},
"player": {
"playListsText": "Fila de Execução",
diff --git a/server/events/events.go b/server/events/events.go
index 8cf15a4d4..12db34b32 100644
--- a/server/events/events.go
+++ b/server/events/events.go
@@ -1,5 +1,7 @@
package events
+import "time"
+
type Event interface {
EventName() string
}
@@ -16,3 +18,9 @@ type KeepAlive struct {
}
func (s KeepAlive) EventName() string { return "keepAlive" }
+
+type ServerStart struct {
+ StartTime time.Time `json:"startTime"`
+}
+
+func (s ServerStart) EventName() string { return "serverStart" }
diff --git a/server/events/sse.go b/server/events/sse.go
index 665173e21..7d684375c 100644
--- a/server/events/sse.go
+++ b/server/events/sse.go
@@ -16,6 +16,8 @@ type Broker interface {
SendMessage(event Event)
}
+var serverStart time.Time
+
type broker struct {
// Events are pushed to this channel by the main events-gathering routine
notifier chan []byte
@@ -46,6 +48,13 @@ func NewBroker() Broker {
}
func (broker *broker) SendMessage(event Event) {
+ data := broker.formatEvent(event)
+
+ log.Trace("Broker received new event", "name", event.EventName(), "payload", string(data))
+ broker.notifier <- data
+}
+
+func (broker *broker) formatEvent(event Event) []byte {
pkg := struct {
Event `json:"data"`
Name string `json:"name"`
@@ -53,9 +62,7 @@ func (broker *broker) SendMessage(event Event) {
pkg.Name = event.EventName()
pkg.Event = event
data, _ := json.Marshal(pkg)
-
- log.Trace("Broker received new event", "name", pkg.Name, "payload", string(data))
- broker.notifier <- data
+ return data
}
func (broker *broker) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@@ -111,27 +118,34 @@ func (broker *broker) listen() {
for {
select {
case s := <-broker.newClients:
-
// A new client has connected.
// Register their message channel
broker.clients[s] = true
log.Debug("Client added to event broker", "numClients", len(broker.clients))
- case s := <-broker.closingClients:
+ // Send a serverStart event to new client
+ s <- broker.formatEvent(&ServerStart{serverStart})
+
+ case s := <-broker.closingClients:
// A client has dettached and we want to
// stop sending them messages.
delete(broker.clients, s)
log.Debug("Removed client from event broker", "numClients", len(broker.clients))
- case event := <-broker.notifier:
+ case event := <-broker.notifier:
// We got a new event from the outside!
// Send event to all connected clients
for clientMessageChan := range broker.clients {
clientMessageChan <- event
}
+
case ts := <-keepAlive.C:
// Send a keep alive packet every 15 seconds
broker.SendMessage(&KeepAlive{TS: ts.Unix()})
}
}
}
+
+func init() {
+ serverStart = time.Now()
+}
diff --git a/ui/src/App.js b/ui/src/App.js
index 059716e61..2b9df8de9 100644
--- a/ui/src/App.js
+++ b/ui/src/App.js
@@ -27,7 +27,7 @@ import createAdminStore from './store/createAdminStore'
import { i18nProvider } from './i18n'
import config from './config'
import { startEventStream } from './eventStream'
-import { processEvent } from './actions'
+
const history = createHashHistory()
if (config.gaTrackingId) {
@@ -60,7 +60,7 @@ const App = () => (
const Admin = (props) => {
const dispatch = useDispatch()
if (config.devActivityMenu) {
- startEventStream((data) => dispatch(processEvent(data)))
+ startEventStream(dispatch)
}
return (
diff --git a/ui/src/actions/serverEvents.js b/ui/src/actions/serverEvents.js
index aad78d253..0ce45f59d 100644
--- a/ui/src/actions/serverEvents.js
+++ b/ui/src/actions/serverEvents.js
@@ -1,6 +1,10 @@
export const EVENT_SCAN_STATUS = 'EVENT_SCAN_STATUS'
+export const EVENT_SERVER_START = 'EVENT_SERVER_START'
-const actionsMap = { scanStatus: EVENT_SCAN_STATUS }
+const actionsMap = {
+ scanStatus: EVENT_SCAN_STATUS,
+ serverStart: EVENT_SERVER_START,
+}
export const processEvent = (data) => {
let type = actionsMap[data.name]
@@ -11,8 +15,12 @@ export const processEvent = (data) => {
}
}
-export const scanStatusUpdate = (data) =>
- processEvent({
- name: EVENT_SCAN_STATUS,
- data: data,
- })
+export const scanStatusUpdate = (data) => ({
+ type: EVENT_SCAN_STATUS,
+ data: data,
+})
+
+export const serverDown = () => ({
+ type: EVENT_SERVER_START,
+ data: {},
+})
diff --git a/ui/src/eventStream.js b/ui/src/eventStream.js
index 981f99f97..f16a520cc 100644
--- a/ui/src/eventStream.js
+++ b/ui/src/eventStream.js
@@ -1,9 +1,13 @@
import { baseUrl } from './utils'
import throttle from 'lodash.throttle'
+import { processEvent, serverDown } from './actions'
let es = null
-let onMessageHandler = null
-let timeOut = null
+let dispatch = null
+let timeout = null
+const defaultIntervalCheck = 20000
+const errorIntervalCheck = 2000
+let currentIntervalCheck = defaultIntervalCheck
const getEventStream = () => {
if (es === null) {
@@ -15,22 +19,23 @@ const getEventStream = () => {
}
// Reestablish the event stream after 20 secs of inactivity
-const setTimeout = () => {
- if (timeOut != null) {
- window.clearTimeout(timeOut)
+const setTimeout = (value) => {
+ currentIntervalCheck = value
+ if (timeout != null) {
+ window.clearTimeout(timeout)
}
- timeOut = window.setTimeout(() => {
+ timeout = window.setTimeout(() => {
if (es != null) {
es.close()
}
es = null
- startEventStream(onMessageHandler)
- }, 20000)
+ startEventStream(dispatch)
+ }, currentIntervalCheck)
}
-export const startEventStream = (messageHandler) => {
- onMessageHandler = messageHandler
- setTimeout()
+export const startEventStream = (dispatchFunc) => {
+ dispatch = dispatchFunc
+ setTimeout(currentIntervalCheck)
if (!localStorage.getItem('token')) {
console.log('Cannot create a unauthenticated EventSource')
return
@@ -40,11 +45,17 @@ export const startEventStream = (messageHandler) => {
(msg) => {
const data = JSON.parse(msg.data)
if (data.name !== 'keepAlive') {
- onMessageHandler(data)
+ dispatch(processEvent(data))
}
- setTimeout() // Reset timeout on every received message
+ setTimeout(defaultIntervalCheck) // Reset timeout on every received message
},
100,
{ trailing: true }
)
+ es.onerror = (e) => {
+ setTimeout(errorIntervalCheck)
+ dispatch(serverDown())
+ }
+
+ return es
}
diff --git a/ui/src/i18n/en.json b/ui/src/i18n/en.json
index 11001b253..f63b7509c 100644
--- a/ui/src/i18n/en.json
+++ b/ui/src/i18n/en.json
@@ -291,7 +291,9 @@
"title": "Activity",
"totalScanned": "Total Folders Scanned",
"quickScan": "Quick Scan",
- "fullScan": "Full Scan"
+ "fullScan": "Full Scan",
+ "serverUptime": "Server Uptime",
+ "serverDown": "OFFLINE"
},
"player": {
"playListsText": "Play Queue",
diff --git a/ui/src/layout/ActivityPanel.js b/ui/src/layout/ActivityPanel.js
index f9d6031ad..bf46df053 100644
--- a/ui/src/layout/ActivityPanel.js
+++ b/ui/src/layout/ActivityPanel.js
@@ -15,14 +15,18 @@ import {
Box,
} from '@material-ui/core'
import { FiActivity } from 'react-icons/fi'
+import { BiError } from 'react-icons/bi'
import { VscSync } from 'react-icons/vsc'
import { GiMagnifyingGlass } from 'react-icons/gi'
import subsonic from '../subsonic'
import { scanStatusUpdate } from '../actions'
+import { useInterval } from '../common'
+import { formatDuration } from '../utils'
const useStyles = makeStyles((theme) => ({
wrapper: {
position: 'relative',
+ color: (props) => (props.up ? null : 'orange'),
},
progress: {
color: theme.palette.primary.light,
@@ -36,17 +40,31 @@ const useStyles = makeStyles((theme) => ({
zIndex: 2,
},
counterStatus: {
- minWidth: '16em',
+ minWidth: '15em',
},
}))
+const getUptime = (serverStart) =>
+ formatDuration((Date.now() - serverStart.startTime) / 1000)
+
+const Uptime = () => {
+ const serverStart = useSelector((state) => state.activity.serverStart)
+ const [uptime, setUptime] = useState(getUptime(serverStart))
+ useInterval(() => {
+ setUptime(getUptime(serverStart))
+ }, 1000)
+ return {uptime}
+}
+
const ActivityPanel = () => {
- const classes = useStyles()
+ const serverStart = useSelector((state) => state.activity.serverStart)
+ const up = serverStart && serverStart.startTime
+ const classes = useStyles({ up })
const translate = useTranslate()
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
- const scanStatus = useSelector((state) => state.activity.scanStatus)
const dispatch = useDispatch()
+ const scanStatus = useSelector((state) => state.activity.scanStatus)
const handleMenuOpen = (event) => setAnchorEl(event.currentTarget)
const handleMenuClose = () => setAnchorEl(null)
@@ -70,7 +88,7 @@ const ActivityPanel = () => {
-
+ {up ? : }
@@ -92,6 +110,17 @@ const ActivityPanel = () => {
onClose={handleMenuClose}
>
+
+
+
+ {translate('activity.serverUptime')}:
+
+
+ {up ? : translate('activity.serverDown')}
+
+
+
+
diff --git a/ui/src/reducers/activityReducer.js b/ui/src/reducers/activityReducer.js
index da19c194c..12abb98f2 100644
--- a/ui/src/reducers/activityReducer.js
+++ b/ui/src/reducers/activityReducer.js
@@ -1,8 +1,10 @@
-import { EVENT_SCAN_STATUS } from '../actions'
+import { EVENT_SCAN_STATUS, EVENT_SERVER_START } from '../actions'
+
+const defaultState = { scanStatus: { scanning: false, count: 0 } }
export const activityReducer = (
previousState = {
- scanStatus: { scanning: false, count: 0 },
+ scanStatus: defaultState,
},
payload
) => {
@@ -10,6 +12,13 @@ export const activityReducer = (
switch (type) {
case EVENT_SCAN_STATUS:
return { ...previousState, scanStatus: data }
+ case EVENT_SERVER_START:
+ return {
+ ...previousState,
+ serverStart: {
+ startTime: data.startTime && Date.parse(data.startTime),
+ },
+ }
default:
return previousState
}