Create backup function

Create backup feature to store data as a json file

Modified Terminal to be more dynamic in the backend to be used by multiple modules, messages are now stored and send from server side instead of in the session.

Added username to last watched items in library activity

fixed navbar home url

Reverted stats to ut nor rely on jf_all_playback_activity as it has not been implemented yet
This commit is contained in:
Thegan Govender
2023-04-23 22:20:14 +02:00
parent 43044979b5
commit 03ab7fc2ff
19 changed files with 417 additions and 87 deletions

View File

@@ -1,10 +1,11 @@
const WebSocket = require('ws');
function createWebSocketServer(port) {
const wss = new WebSocket.Server({ port });
function createWebSocketServer() {
var messages=[];
const port=process.env.WS_PORT || 3004 ;
const wss = new WebSocket.Server({port});
// function to handle WebSocket connections
function handleConnection(ws) {
console.log('Client connected');
@@ -17,24 +18,30 @@ function createWebSocketServer(port) {
ws.on('close', () => {
console.log('Client disconnected');
});
}
ws.send(JSON.stringify(messages));
}
// call the handleConnection function for each new WebSocket connection
wss.on('connection', handleConnection);
// define a separate method that sends a message to all connected clients
function sendMessageToClients(message) {
messages.push(message);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
let parsedMessage = JSON.stringify(message);
console.log(parsedMessage);
client.send(parsedMessage);
client.send(JSON.stringify(messages));
}
});
}
return sendMessageToClients;
function clearMessages() {
messages=[];
}
return {sendMessageToClients, clearMessages, wss};
}
module.exports = createWebSocketServer;
const wsServer = createWebSocketServer();
module.exports = wsServer;

View File

@@ -187,7 +187,7 @@ router.get("/getHistory", async (req, res) => {
const { rows } = await db.query(
`SELECT * FROM jf_playback_activity order by "ActivityDateInserted" desc`
`SELECT * FROM jf_all_playback_activity order by "ActivityDateInserted" desc`
);
const groupedResults = {};

View File

@@ -1,7 +1,13 @@
const { Router } = require('express');
const { Pool } = require('pg');
const fs = require('fs');
const readline = require('readline');
const path = require('path');
const moment = require('moment');
const wss = require("./WebsocketHandler");
var messages=[];
const router = Router();
@@ -13,7 +19,7 @@ const postgresPort = process.env.POSTGRES_PORT;
const postgresDatabase = process.env.POSTGRES_DATABASE || 'jfstat';
// Tables to back up
const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_library_episodes','jf_users','jf_playback_activity'];
const tables = ['jf_libraries', 'jf_library_items', 'jf_library_seasons','jf_library_episodes','jf_users','jf_playback_activity','jf_playback_reporting_plugin_data','jf_item_info'];
// Backup function
async function backup() {
@@ -26,19 +32,25 @@ async function backup() {
});
// Get data from each table and append it to the backup file
const backupPath = './backup-data/backup.json';
let now = moment();
const backupPath = `./backup-data/backup_${now.format('yyyy-MM-DD HH-mm-ss')}.json`;
const stream = fs.createWriteStream(backupPath, { flags: 'a' });
const backup_data=[];
wss.clearMessages();
wss.sendMessageToClients({ color: "yellow", Message: "Begin Backup "+backupPath });
for (let table of tables) {
const query = `SELECT * FROM ${table}`;
const { rows } = await pool.query(query);
console.log(`Reading ${rows.length} rows for table ${table}`);
wss.sendMessageToClients({color: "dodgerblue",Message: `Saving ${rows.length} rows for table ${table}`});
backup_data.push({[table]:rows});
backup_data.push({[table]:rows});
// stream.write(JSON.stringify(backup_data));
}
wss.sendMessageToClients({ color: "lawngreen", Message: "Backup Complete" });
stream.write(JSON.stringify(backup_data));
stream.end();
@@ -151,6 +163,55 @@ router.get('/restore', async (req, res) => {
res.status(500).send('Backup failed');
}
});
//list backup files
const backupfolder='backup-data';
router.get('/files', (req, res) => {
const directoryPath = path.join(__dirname, backupfolder);
fs.readdir(directoryPath, (err, files) => {
if (err) {
res.status(500).send('Unable to read directory');
} else {
const fileData = files.map(file => {
const filePath = path.join(directoryPath, file);
const stats = fs.statSync(filePath);
return {
name: file,
size: stats.size,
datecreated: stats.birthtime
};
});
res.json(fileData);
}
});
});
//download backup file
router.get('/files/:filename', (req, res) => {
const filePath = path.join(__dirname, backupfolder, req.params.filename);
res.download(filePath);
});
//delete backup
router.delete('/files/:filename', (req, res) => {
const filePath = path.join(__dirname, backupfolder, req.params.filename);
fs.unlink(filePath, (err) => {
if (err) {
console.error(err);
res.status(500).send('An error occurred while deleting the file.');
return;
}
console.log(`${filePath} has been deleted.`);
res.status(200).send(`${filePath} has been deleted.`);
});
});

View File

@@ -51,7 +51,7 @@ app.use('/auth', authRouter); // mount the API router at /api, with JWT middlewa
app.use('/api', verifyToken, apiRouter); // mount the API router at /api, with JWT middleware
app.use('/sync', verifyToken, syncRouter); // mount the API router at /sync, with JWT middleware
app.use('/stats', verifyToken, statsRouter); // mount the API router at /stats, with JWT middleware
app.use('/data', backupRouter); // mount the API router at /stats, with JWT middleware
app.use('/data', verifyToken, backupRouter); // mount the API router at /stats, with JWT middleware
try{
createdb.createDatabase().then((result) => {

View File

@@ -419,7 +419,7 @@ router.post("/getGlobalItemStats", async (req, res) => {
const { rows } = await db.query(
`select count(*)"Plays",
sum("PlaybackDuration") total_playback_duration
from jf_playback_activity
from jf_all_playback_activity jf_playback_activity
where
("EpisodeId"='${itemid}' OR "SeasonId"='${itemid}' OR "NowPlayingItemId"='${itemid}')
AND jf_playback_activity."ActivityDateInserted" BETWEEN CURRENT_DATE - INTERVAL '1 hour' * ${_hours} AND NOW();`

View File

@@ -3,8 +3,9 @@ const pgp = require("pg-promise")();
const db = require("./db");
const axios = require("axios");
const ws = require("./WebsocketHandler");
const sendMessageToClients = ws(process.env.WS_PORT || 3004);
const wss = require("./WebsocketHandler");
const socket=wss;
const router = express.Router();
@@ -134,7 +135,7 @@ async function syncUserData()
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
res.send({ error: "Config Details Not Found" });
sendMessageToClients({ Message: "Error: Config details not found!" });
socket.sendMessageToClients({ Message: "Error: Config details not found!" });
return;
}
@@ -160,9 +161,9 @@ async function syncUserData()
if (dataToInsert.length !== 0) {
let result = await db.insertBulk("jf_users",dataToInsert,jf_users_columns);
if (result.Result === "SUCCESS") {
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
socket.sendMessageToClients(dataToInsert.length + " Rows Inserted.");
} else {
sendMessageToClients({
socket.sendMessageToClients({
color: "red",
Message: "Error performing bulk insert:" + result.message,
});
@@ -173,9 +174,9 @@ async function syncUserData()
if (toDeleteIds.length > 0) {
let result = await db.deleteBulk("jf_users",toDeleteIds);
if (result.Result === "SUCCESS") {
sendMessageToClients(toDeleteIds.length + " Rows Removed.");
socket.sendMessageToClients(toDeleteIds.length + " Rows Removed.");
} else {
sendMessageToClients({color: "red",Message: result.message,});
socket.sendMessageToClients({color: "red",Message: result.message,});
}
}
@@ -187,7 +188,7 @@ async function syncLibraryFolders()
const { rows } = await db.query('SELECT * FROM app_config where "ID"=1');
if (rows[0].JF_HOST === null || rows[0].JF_API_KEY === null) {
res.send({ error: "Config Details Not Found" });
sendMessageToClients({ Message: "Error: Config details not found!" });
socket.sendMessageToClients({ Message: "Error: Config details not found!" });
return;
}
@@ -213,9 +214,9 @@ async function syncLibraryFolders()
if (dataToInsert.length !== 0) {
let result = await db.insertBulk("jf_libraries",dataToInsert,jf_libraries_columns);
if (result.Result === "SUCCESS") {
sendMessageToClients(dataToInsert.length + " Rows Inserted.");
socket.sendMessageToClients(dataToInsert.length + " Rows Inserted.");
} else {
sendMessageToClients({
socket.sendMessageToClients({
color: "red",
Message: "Error performing bulk insert:" + result.message,
});
@@ -226,9 +227,9 @@ async function syncLibraryFolders()
if (toDeleteIds.length > 0) {
let result = await db.deleteBulk("jf_libraries",toDeleteIds);
if (result.Result === "SUCCESS") {
sendMessageToClients(toDeleteIds.length + " Rows Removed.");
socket.sendMessageToClients(toDeleteIds.length + " Rows Removed.");
} else {
sendMessageToClients({color: "red",Message: result.message,});
socket.sendMessageToClients({color: "red",Message: result.message,});
}
}
@@ -244,9 +245,9 @@ async function syncLibraryItems()
}
const _sync = new sync(config[0].JF_HOST, config[0].JF_API_KEY);
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/3" });
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 1/3" });
sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",});
socket.sendMessageToClients({color: "yellow",Message: "Beginning Library Item Sync",});
const admins = await _sync.getAdminUser();
const userid = admins[0].Id;
@@ -289,7 +290,7 @@ async function syncLibraryItems()
if (result.Result === "SUCCESS") {
insertCounter += dataToInsert.length;
} else {
sendMessageToClients({
socket.sendMessageToClients({
color: "red",
Message: "Error performing bulk insert:" + result.message,
});
@@ -302,23 +303,23 @@ async function syncLibraryItems()
if (result.Result === "SUCCESS") {
deleteCounter +=toDeleteIds.length;
} else {
sendMessageToClients({color: "red",Message: result.message,});
socket.sendMessageToClients({color: "red",Message: result.message,});
}
}
sendMessageToClients({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",});
sendMessageToClients({color: "orange",Message: deleteCounter + " Library Items Removed.",});
sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" });
socket.sendMessageToClients({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",});
socket.sendMessageToClients({color: "orange",Message: deleteCounter + " Library Items Removed.",});
socket.sendMessageToClients({ color: "yellow", Message: "Item Sync Complete" });
// const { rows: cleanup } = await db.query('DELETE FROM jf_playback_activity where "NowPlayingItemId" not in (select "Id" from jf_library_items)' );
// sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" });
// socket.sendMessageToClients({ color: "orange", Message: cleanup.length+" orphaned activity logs removed" });
}
async function syncShowItems()
{
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/3" });
sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",});
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 2/3" });
socket.sendMessageToClients({color: "yellow", Message: "Beginning Seasons and Episode sync",});
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
@@ -387,7 +388,7 @@ async function syncShowItems()
if (result.Result === "SUCCESS") {
insertSeasonsCount += seasonsToInsert.length;
} else {
sendMessageToClients({
socket.sendMessageToClients({
color: "red",
Message: "Error performing bulk insert:" + result.message,
});
@@ -400,7 +401,7 @@ async function syncShowItems()
if (result.Result === "SUCCESS") {
deleteSeasonsCount +=toDeleteIds.length;
} else {
sendMessageToClients({color: "red",Message: result.message,});
socket.sendMessageToClients({color: "red",Message: result.message,});
}
}
@@ -411,7 +412,7 @@ async function syncShowItems()
if (result.Result === "SUCCESS") {
insertEpisodeCount += episodesToInsert.length;
} else {
sendMessageToClients({
socket.sendMessageToClients({
color: "red",
Message: "Error performing bulk insert:" + result.message,
});
@@ -425,25 +426,25 @@ async function syncShowItems()
if (result.Result === "SUCCESS") {
deleteEpisodeCount +=toDeleteEpisodeIds.length;
} else {
sendMessageToClients({color: "red",Message: result.message,});
socket.sendMessageToClients({color: "red",Message: result.message,});
}
}
sendMessageToClients({ Message: "Sync complete for " + show.Name });
socket.sendMessageToClients({ Message: "Sync complete for " + show.Name });
}
sendMessageToClients({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",});
sendMessageToClients({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
sendMessageToClients({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",});
sendMessageToClients({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
sendMessageToClients({ color: "yellow", Message: "Sync Complete" });
socket.sendMessageToClients({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",});
socket.sendMessageToClients({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
socket.sendMessageToClients({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",});
socket.sendMessageToClients({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
socket.sendMessageToClients({ color: "yellow", Message: "Sync Complete" });
}
async function syncItemInfo()
{
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
socket.sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
const { rows: config } = await db.query('SELECT * FROM app_config where "ID"=1');
@@ -485,7 +486,7 @@ async function syncItemInfo()
if (result.Result === "SUCCESS") {
insertItemInfoCount += ItemInfoToInsert.length;
} else {
sendMessageToClients({
socket.sendMessageToClients({
color: "red",
Message: "Error performing bulk insert:" + result.message,
});
@@ -498,7 +499,7 @@ async function syncItemInfo()
if (result.Result === "SUCCESS") {
deleteItemInfoCount +=toDeleteItemInfoIds.length;
} else {
sendMessageToClients({color: "red",Message: result.message,});
socket.sendMessageToClients({color: "red",Message: result.message,});
}
}
@@ -529,7 +530,7 @@ async function syncItemInfo()
if (result.Result === "SUCCESS") {
insertEpisodeInfoCount += EpisodeInfoToInsert.length;
} else {
sendMessageToClients({
socket.sendMessageToClients({
color: "red",
Message: "Error performing bulk insert:" + result.message,
});
@@ -542,24 +543,24 @@ async function syncItemInfo()
if (result.Result === "SUCCESS") {
deleteEpisodeInfoCount +=toDeleteEpisodeInfoIds.length;
} else {
sendMessageToClients({color: "red",Message: result.message,});
socket.sendMessageToClients({color: "red",Message: result.message,});
}
}
console.log(Episode.Name)
}
sendMessageToClients({color: "dodgerblue",Message: insertItemInfoCount + " Item Info inserted.",});
sendMessageToClients({color: "orange",Message: deleteItemInfoCount + " Item Info Removed.",});
sendMessageToClients({color: "dodgerblue",Message: insertEpisodeInfoCount + " Episodes Info inserted.",});
sendMessageToClients({color: "orange",Message: deleteEpisodeInfoCount + " Episodes Info Removed.",});
sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" });
socket.sendMessageToClients({color: "dodgerblue",Message: insertItemInfoCount + " Item Info inserted.",});
socket.sendMessageToClients({color: "orange",Message: deleteItemInfoCount + " Item Info Removed.",});
socket.sendMessageToClients({color: "dodgerblue",Message: insertEpisodeInfoCount + " Episodes Info inserted.",});
socket.sendMessageToClients({color: "orange",Message: deleteEpisodeInfoCount + " Episodes Info Removed.",});
socket.sendMessageToClients({ color: "lawngreen", Message: "Sync Complete" });
}
async function syncPlaybackPluginData()
{
sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
socket.sendMessageToClients({ color: "lawngreen", Message: "Syncing... 3/3" });
socket.sendMessageToClients({color: "yellow", Message: "Beginning File Info Sync",});
try {
const { rows: config } = await db.query(
@@ -624,12 +625,15 @@ async function syncPlaybackPluginData()
///////////////////////////////////////Sync All
router.get("/beingSync", async (req, res) => {
socket.clearMessages();
await syncUserData();
await syncLibraryFolders();
await syncLibraryItems();
await syncShowItems();
await syncItemInfo();
res.send();
});

View File

@@ -55,6 +55,13 @@ function LastWatchedCard(props) {
<div className="last-last-played">
{formatTime(props.data.LastPlayed)}
</div>
<div className="pb-2">
<Link to={`/users/${props.data.UserId}`}>
{props.data.UserName}
</Link>
</div>
<div className="last-item-name"> {props.data.Name}</div>
<div className="last-item-episode"> {props.data.EpisodeName}</div>
</div>

View File

@@ -15,7 +15,7 @@ export default function Navbar() {
return (
<BootstrapNavbar variant="dark" expand="md" className="navbar py-0">
<Container fluid>
<BootstrapNavbar.Brand href="#home">Jellystat</BootstrapNavbar.Brand>
<BootstrapNavbar.Brand as={Link} to={"/"}>Jellystat</BootstrapNavbar.Brand>
<BootstrapNavbar.Toggle aria-controls="responsive-navbar-nav" />
<BootstrapNavbar.Collapse id="responsive-navbar-nav">
<Nav className="ms-auto">

View File

@@ -12,7 +12,7 @@ const TerminalComponent = () => {
// handle incoming messages
socket.addEventListener('message', (event) => {
let message = JSON.parse(event.data);
setMessages(prevMessages => [...prevMessages, message]);
setMessages(message);
});
// cleanup function to close the WebSocket connection when the component unmounts

View File

@@ -0,0 +1,163 @@
import React, { useState,useEffect } from "react";
import axios from "axios";
import Button from "react-bootstrap/Button";
import Alert from "react-bootstrap/Alert";
import "../../css/settings/backups.css";
import { Table } from "react-bootstrap";
export default function BackupFiles() {
const [files, setFiles] = useState([]);
const [showAlert, setshowAlert] = useState({visible:false,type:'danger',title:'Error',message:''});
const token = localStorage.getItem('token');
useEffect(() => {
const fetchData = async () => {
try {
const backupFiles = await axios.get(`/data/files`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
setFiles(backupFiles.data);
} catch (error) {
console.log(error);
}
};
fetchData();
const intervalId = setInterval(fetchData, 60000 * 5);
return () => clearInterval(intervalId);
}, [files,token]);
async function downloadBackup(filename) {
const url=`/data/files/${filename}`;
axios({
url: url,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
method: 'GET',
responseType: 'blob',
}).then(response => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
});
}
async function deleteBackup(filename) {
const url=`/data/files/${filename}`;
axios
.delete(url, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
.then((response) => {
setshowAlert({visible:true,title:'Success',type:'success',message:response.data});
})
.catch((error) => {
setshowAlert({visible:true,title:'Error',type:'danger',message:error.response.data});
});
}
function formatFileSize(sizeInBytes) {
const sizeInKB = sizeInBytes / 1024; // 1 KB = 1024 bytes
if (sizeInKB < 1024) {
return `${sizeInKB.toFixed(2)} KB`;
} else {
const sizeInMB = sizeInKB / 1024; // 1 MB = 1024 KB
if (sizeInMB < 1024) {
return `${sizeInMB.toFixed(2)} MB`;
} else {
const sizeInGB = sizeInMB / 1024; // 1 GB = 1024 MB
if (sizeInGB < 1024) {
return `${sizeInGB.toFixed(2)} GB`;
} else {
const sizeInTB = sizeInGB / 1024; // 1 TB = 1024 GB
if (sizeInTB < 1024) {
return `${sizeInTB.toFixed(2)} TB`;
} else {
const sizeInPB = sizeInTB / 1024; // 1 PB = 1024 TB
return `${sizeInPB.toFixed(2)} PB`;
}
}
}
}
}
const options = {
day: "numeric",
month: "numeric",
year: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: false,
};
function handleCloseAlert() {
setshowAlert({visible:false});
}
return (
<div>
<h1 className="my-2">Backups</h1>
{showAlert && showAlert.visible && (
<Alert variant={showAlert.type} onClose={handleCloseAlert} dismissible>
<Alert.Heading>{showAlert.title}</Alert.Heading>
<p>
{showAlert.message}
</p>
</Alert>
)}
<Table>
<thead>
<tr>
<th>File Name</th>
<th>Date Created</th>
<th>Size</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{files &&
files.sort((a, b) =>new Date(b.datecreated) - new Date(a.datecreated)).map((file, index) => (
<tr key={index}>
<td>{file.name}</td>
<td>{Intl.DateTimeFormat('en-UK', options).format(new Date(file.datecreated))}</td>
<td>{formatFileSize(file.size)}</td>
<td ><Button type="button" onClick={()=>downloadBackup(file.name)} >Download</Button></td>
<td ><Button type="button" className="btn-danger" onClick={()=>deleteBackup(file.name)} >Delete</Button></td>
</tr>
))}
{files.length===0 ? <tr><td colSpan="5" style={{ textAlign: "center", fontStyle: "italic" ,color:"gray"}}>No Backups Found</td></tr> :''}
</tbody>
</Table>
</div>
);
}

View File

@@ -6,7 +6,7 @@ import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import "../../css/settings.css";
import "../../css/settings/settings.css";
export default function LibrarySync() {
const [processing, setProcessing] = useState(false);
@@ -35,6 +35,30 @@ export default function LibrarySync() {
// return { isValid: isValid, errorMessage: errorMessage };
}
async function createBackup() {
setProcessing(true);
await axios
.get("/data/backup", {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 200) {
// isValid = true;
}
})
.catch((error) => {
console.log(error);
});
setProcessing(false);
// return { isValid: isValid, errorMessage: errorMessage };
}
const handleClick = () => {
beginSync();
@@ -51,7 +75,18 @@ export default function LibrarySync() {
</Form.Label>
<Col sm="10">
<Button variant={!processing ? "outline-primary" : "outline-light"} disabled={processing} onClick={handleClick}>Run Sync</Button>
<Button variant={!processing ? "outline-primary" : "outline-light"} disabled={processing} onClick={handleClick}>Start</Button>
</Col>
</Row>
<Row className="mb-3">
<Form.Label column sm="2">
Create Backup
</Form.Label>
<Col sm="10">
<Button variant={!processing ? "outline-primary" : "outline-light"} disabled={processing} onClick={createBackup}>Start</Button>
</Col>
</Row>

View File

@@ -10,7 +10,7 @@ import Alert from 'react-bootstrap/Alert';
import "../../css/settings.css";
import "../../css/settings/settings.css";
import { ButtonGroup } from "react-bootstrap";
export default function SettingsConfig() {

View File

@@ -0,0 +1,22 @@
tr{
color: white;
}
th:hover{
border-bottom: none !important;
}
th{
border-bottom: none !important;
cursor: default !important;
background-color: rgba(0, 0, 0, 0.8) !important;
}
.backup-file-download
{
cursor: pointer;
}
td{
border-bottom: none !important;
}

View File

@@ -12,7 +12,7 @@
/* margin: 25px 0; */
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
/* min-width: 400px; */
/* box-shadow: 0 0 20px rgba(255, 255, 255, 0.15); */
background-color: rgba(100,100, 100, 0.2);
color: white;
@@ -20,16 +20,27 @@
}
th,
td
{
padding: 15px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
@media screen and (max-width: 576px) {
td[data-cell]::before {
content: attr(data-cell)": ";
font-weight: bold;
text-transform: capitalize;
color: #a8a8a8;
}
}
td a{
text-decoration: none;
color: white;
@@ -43,16 +54,24 @@ td:hover a{
th {
padding: 15px 15px;
background-color: rgba(0, 0, 0, 0.8);
border-right: 1px solid rgba(255, 255, 255, 0.05);
border-bottom: 1px solid transparent !important;
cursor: pointer;
}
th:hover {
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
border-bottom: 1px solid rgba(255, 255, 255, 0.5) !important;
}
tr:nth-child(even) {
background-color: rgba(100, 100, 100, 0.1);
}
tr:nth-child(odd) {
background-color: rgba(0, 0, 0, 0.1);
}
/* tbody tr:last-of-type {
border-bottom: 2px solid #009879;

View File

@@ -5,7 +5,6 @@
overflow-y: auto;
margin-top: 20px;
padding: 10px;
margin-right: 10px;
border-radius: 8px;
}

View File

@@ -3,9 +3,13 @@ import React from "react";
import SettingsConfig from "./components/settings/settingsConfig";
import LibrarySync from "./components/settings/librarySync";
import BackupFiles from "./components/settings/backupfiles";
import TerminalComponent from "./components/settings/TerminalComponent";
import "./css/settings.css";
import "./css/settings/settings.css";
export default function Settings() {
@@ -13,7 +17,9 @@ export default function Settings() {
return (
<div>
<SettingsConfig/>
<BackupFiles/>
<LibrarySync/>
<TerminalComponent/>
</div>

View File

@@ -155,19 +155,19 @@ function Users() {
<table className="user-activity-table">
<thead>
<tr>
<th></th>
<th onClick={() => handleSort("UserName")}>User</th>
<th onClick={() => handleSort("LastWatched")}>Last Watched</th>
<th onClick={() => handleSort("LastClient")}>Last Client</th>
<th onClick={() => handleSort("TotalPlays")}>Total Plays</th>
<th onClick={() => handleSort("TotalWatchTime")}>Total Watch Time</th>
<th onClick={() => handleSort("LastSeen")}>Last Seen</th>
<th className="d-none d-md-table-cell" ></th>
<th className="d-none d-md-table-cell" onClick={() => handleSort("UserName")}>User</th>
<th className="d-none d-md-table-cell" onClick={() => handleSort("LastWatched")}>Last Watched</th>
<th className="d-none d-md-table-cell" onClick={() => handleSort("LastClient")}>Last Client</th>
<th className="d-none d-md-table-cell" onClick={() => handleSort("TotalPlays")}>Total Plays</th>
<th className="d-none d-md-table-cell" onClick={() => handleSort("TotalWatchTime")}>Total Watch Time</th>
<th className="d-none d-md-table-cell" onClick={() => handleSort("LastSeen")}>Last Seen</th>
</tr>
</thead>
<tbody>
{currentUsers.map((item) => (
<tr key={item.UserId}>
<td>
<tr key={item.UserId} >
<td className="d-block d-md-table-cell">
{item.PrimaryImageTag ? (
<img
className="card-user-image"
@@ -183,12 +183,12 @@ function Users() {
<AccountCircleFillIcon color="#fff" size={30} />
)}
</td>
<td> <Link to={`/users/${item.UserId}`}>{item.UserName}</Link></td>
<td>{item.LastWatched || 'never'}</td>
<td>{item.LastClient || 'n/a'}</td>
<td>{item.TotalPlays}</td>
<td>{formatTotalWatchTime(item.TotalWatchTime) || 0}</td>
<td>{item.LastSeen ? formatLastSeenTime(item.LastSeen) : 'never'}</td>
<td className="d-block d-md-table-cell py-2" data-cell={"User"}> <Link to={`/users/${item.UserId}`}>{item.UserName}</Link></td>
<td className="d-block d-md-table-cell py-2" data-cell={"Last Watched"}>{item.LastWatched || 'never'}</td>
<td className="d-block d-md-table-cell py-2" data-cell={"Last Client"}>{item.LastClient || 'n/a'}</td>
<td className="d-block d-md-table-cell py-2" data-cell={"Total Plays"}>{item.TotalPlays}</td>
<td className="d-block d-md-table-cell py-2" data-cell={"Total Watch Time"}>{formatTotalWatchTime(item.TotalWatchTime) || 0}</td>
<td className="d-block d-md-table-cell py-2" data-cell={"Last Seen"}>{item.LastSeen ? formatLastSeenTime(item.LastSeen) : 'never'}</td>
</tr>
))}
</tbody>

View File

@@ -29,6 +29,13 @@ module.exports = function(app) {
changeOrigin: true,
})
);
app.use(
'/data',
createProxyMiddleware({
target: 'http://127.0.0.1:3003',
changeOrigin: true,
})
);
app.use(
'/ws',
createProxyMiddleware({