Compare commits

..

23 Commits

Author SHA1 Message Date
03dab84224 Started conversion to bootstrap for the frontend framework 2025-07-01 14:12:59 -04:00
604025179c Converting from SASS to SCSS 2025-07-01 13:56:45 -04:00
e06f3274ec Added a new scripts block to the default layout 2025-07-01 13:49:00 -04:00
089b2289c7 Converted from coffeescript to javascript for frontend scripts (to much pain) 2025-07-01 13:44:08 -04:00
7170856587 Added docker container to run grunt; updated workdir paths in docker containers 2025-06-30 11:49:28 -04:00
f80571ba78 Updated the formatting of the recently updated tests table on the dashboard 2025-06-23 18:58:36 -04:00
f43f27ec1a Removed old 'latest results' section from the dashboard 2025-06-23 18:57:19 -04:00
ebd5337a6c Added some linkage around the app between tests, benchmarks and hardware 2025-06-23 18:56:19 -04:00
5394959880 Fixed some formatting of the benchmark scoring 2025-06-23 18:42:00 -04:00
2b5193dccf Added ability to add benchmark results to a test 2025-06-23 18:39:01 -04:00
c1da3b6a57 Cleaned up some CoffeeScript syntax 2025-06-23 18:07:21 -04:00
175bfa0dfd Added ability to load benchmark results when viewing a test 2025-06-23 18:05:28 -04:00
a83f7e3b0b Fixed typo in Grunt config to watch for the correct .coffee file type 2025-06-23 17:56:51 -04:00
e9a4d85c1d Upgrading package versions 2025-06-11 12:45:25 -04:00
7375c1ce39 Changing database schema a bit 2024-06-08 09:47:48 -04:00
584a6087ba Migrated to SASS and CoffeeScript for stylesheets and scripting 2024-06-06 23:24:28 -04:00
facc252d3b Fixed list of tests on the dashboard 2024-02-24 19:06:34 -05:00
6761aaa413 Started adding results 2024-02-24 01:34:32 -05:00
65fc2b753b Added support for Docker usage in development 2024-02-23 13:27:29 -05:00
75552eccee Removed projects; added tests 2023-12-02 23:41:04 -05:00
286219c5ea Reorganized routes code into a separate file 2023-11-27 21:29:52 -05:00
34faecc52c Added views and routes for benchmarks 2023-11-27 00:42:42 -05:00
2f016d3062 Added benchmark and hardware models; added routes and views for hardware 2023-11-26 12:54:09 -05:00
43 changed files with 3917 additions and 682 deletions

4
.gitignore vendored
View File

@ -132,3 +132,7 @@ dist
# Local data files
data/*
# Compiled stylesheets and javascripts
public/css/
public/js/

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:24
WORKDIR /usr/src/leviathan
COPY package*.json ./
RUN npm install
VOLUME /usr/src/leviathan/node_modules
COPY . ./
ENTRYPOINT ["npm", "run", "dev"]

13
Dockerfile.grunt Normal file
View File

@ -0,0 +1,13 @@
# Node.js runtime
FROM node:24
WORKDIR /usr/src/leviathan/
COPY package.* /usr/src/leviathan/
RUN npm install
VOLUME /usr/src/leviathan/node_modules/
# Run the app
CMD [ "npm", "run", "grunt" ]

65
Gruntfile.js Normal file
View File

@ -0,0 +1,65 @@
module.exports = function (grunt) {
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON("package.json"),
sass: {
dist: {
options: {
style: "compressed",
},
files: [
{
expand: true,
cwd: "assets/styles",
src: ["**/*.scss"],
dest: "public/css",
ext: ".css",
},
],
},
},
uglify: {
options: {
sourceMap: true,
compress: true,
},
files: {
expand: true,
flatten: true,
cwd: "assets/scripts",
src: ["*.js"],
dest: "public/js",
ext: ".js",
},
},
watch: {
css: {
files: ["assets/styles/**/*.scss"],
tasks: ["sass"],
options: {
atBegin: true,
spawn: false,
},
},
js: {
files: ["assets/scripts/**/*.js"],
tasks: ["uglify"],
options: {
atBegin: true,
spawn: false,
},
},
},
});
// Load plugins.
grunt.loadNpmTasks("grunt-contrib-watch");
grunt.loadNpmTasks("grunt-contrib-sass");
grunt.loadNpmTasks("grunt-contrib-uglify");
// CLI tasks.
grunt.registerTask("default", ["sass", "uglify"]);
};

3
assets/scripts/scar.js Normal file
View File

@ -0,0 +1,3 @@
$(document).ready(function () {
console.log("Ready.");
});

82
assets/scripts/test.js Normal file
View File

@ -0,0 +1,82 @@
const testId = $("#results-table").data("test-id");
$("#test-result-form").on("submit", function (e) {
e.preventDefault();
const form = $(this);
const formData = form.serialize();
const benchmarkId = form.find('[name="result_benchmark"]').val();
$.post("/api/v1/result/add", formData, function (response) {
if (response === "success") {
fetchTestBenchmarkResults(testId, benchmarkId);
form[0].reset();
}
});
});
async function fetchTestBenchmarkResults(testId, benchmarkId) {
try {
const benchmarkSearchParams = new URLSearchParams({
benchmark_id: benchmarkId,
});
const benchmarkRes = await fetch(
`/api/v1/benchmark/details?${benchmarkSearchParams}`,
);
const benchmarkData = await benchmarkRes.json();
const resultSearchParams = new URLSearchParams({
test_id: testId,
benchmark_id: benchmarkId,
});
const resultRes = await fetch(`/api/v1/result/list?${resultSearchParams}`);
const resultData = await resultRes.json();
let avg_total = 0;
let min_total = 0;
let max_total = 0;
for (const result of resultData) {
avg_total += result.avgScore;
if (result.minScore !== undefined) min_total += result.minScore;
if (result.maxScore !== undefined) max_total += result.maxScore;
}
const tableRow = $(`#results-table tr[data-benchmark-id=${benchmarkId}]`);
tableRow.empty();
tableRow.append(
'<td><a href="/benchmark/' +
benchmarkData.id +
'">' +
benchmarkData.name +
"</a></td>",
);
tableRow.append("<td>" + benchmarkData.scoring + "</td>");
tableRow.append("<td>" + resultData.length + "</td>");
if (resultData.length !== 0) {
tableRow.append("<td>" + avg_total / resultData.length + "</td>");
} else {
tableRow.append("<td>N/a</td>");
}
if (min_total !== 0) {
tableRow.append("<td>" + min_total / resultData.length + "</td>");
tableRow.append("<td>" + max_total / resultData.length + "</td>");
} else {
tableRow.append("<td>N/a</td>");
tableRow.append("<td>N/a</td>");
}
} catch (error) {
console.error("An error occurred while fetching benchmark results.", error);
}
}
$(document).ready(function() {
$("#results-table tbody tr").each(function (index, tr) {
const benchmarkId = $(tr).data("benchmark-id");
console.log("Fetching results for benchmark id: " + benchmarkId);
fetchTestBenchmarkResults(testId, benchmarkId);
});
});

View File

@ -0,0 +1,71 @@
html,
body
width: 100%
height: 100%
margin: 0
padding: 0
body
height: auto
min-height: 100%
box-sizing: border-box
padding-top: 80px
padding-bottom: 80px
background: #eee
a
color: teal
transition: all 200ms ease-in-out
&:hover
color: #007070
textarea
max-width: 100%
min-width: 100%
height: 100px
.container
max-width: 1024px
select[multiple]
min-height: 125px
#main-nav
position: fixed
top: 0
left: 0
width: 100%
height: 64px
background: teal
color: white
z-index: 20
ul
list-style: none
display: inline-block
li
display: inline-block
margin-left: 15px
h4
display: inline-block
margin-left: 25px
line-height: 64px
a
color: white
font-size: 2.25rem
line-height: 64px
transition: all 200ms ease-in-out
&:hover
color: #eee
font-size: 2.5rem
#main-content
padding: 14px 20px
background: white
border-radius: 8px
z-index: 10
#main-footer
position: fixed
bottom: 0
left: 0
width: 100%
height: 64px
p
margin-bottom: 5px
text-align: center
#result_form
margin-bottom: 0
*
margin-bottom: 0

5
assets/styles/eye.scss Normal file
View File

@ -0,0 +1,5 @@
$primary-color: teal;
body {
background: white;
}

2
bin/docker-build.bat Normal file
View File

@ -0,0 +1,2 @@
docker build -t leviathan .
docker build -t leviathan-grunt -f Dockerfile.grunt .

4
bin/docker-build.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
docker build -t leviathan .
docker build -t leviathan-grunt -f Dockerfile.grunt .

2
bin/docker-run.bat Normal file
View File

@ -0,0 +1,2 @@
docker run --rm -d -w "/usr/src/leviathan" -v "$(pwd):/usr/src/leviathan" -p 3000:3000 --name leviathan leviathan
docker run --rm -d -w "/usr/src/leviathan" -v "$(pwd):/usr/src/leviathan" --name leviathan-grunt leviathan-grunt npm run grunt watch

4
bin/docker-run.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
docker run --rm -d -w "/usr/src/leviathan" -v "$(pwd):/usr/src/leviathan" -p 3000:3000 --name leviathan leviathan
docker run --rm -d -w "/usr/src/leviathan" -v "$(pwd):/usr/src/leviathan" --name leviathan-grunt leviathan-grunt npm run grunt watch

View File

@ -28,21 +28,16 @@ app.use(bodyParser.urlencoded({ extended: true }));
// enable the Twig template engine
app.set('view engine', 'twig');
app.set('twig options', {
allowAsync: true,
strict_variables: false
});
// enable serving static files
app.use(express.static('public'));
// load routes
const indexRoutes = require('./src/routes/index');
const projectRoutes = require('./src/routes/project');
// register routes
app.get('/', indexRoutes.getIndex);
app.get('/project', projectRoutes.getIndex);
app.get('/project/list', projectRoutes.getList);
app.get('/project/:project_id', projectRoutes.getView);
app.get('/project/add', projectRoutes.getAdd);
app.post('/project/add', projectRoutes.postAdd);
// load routes to express
require('./src/routes')(app);
app.listen(port, () => {
console.log(`Leviathan listening on port ${port}`);

3312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,4 @@
{
"dependencies": {
"express": "^4.18.2",
"express-session": "^1.17.3",
"sequelize": "^6.35.1",
"sqlite3": "^5.1.6",
"twig": "^1.16.0"
},
"name": "leviathan",
"description": "PC hardware benchmarking data logger",
"version": "0.1.0",
@ -13,7 +6,8 @@
"scripts": {
"start": "node index.js",
"dev": "nodemon ./index.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"grunt": "grunt"
},
"repository": {
"type": "git",
@ -26,7 +20,20 @@
],
"author": "Gregory Ballantine <gballantine@metaunix.net>",
"license": "BSD-2-Clause",
"dependencies": {
"express": "^5.1.0",
"express-session": "^1.18.1",
"sequelize": "^6.37.7",
"sqlite3": "^5.1.7",
"twig": "^1.17.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
"grunt": "^1.6.1",
"grunt-cli": "^1.5.0",
"grunt-contrib-sass": "^2.0.0",
"grunt-contrib-uglify": "^5.2.2",
"grunt-contrib-watch": "^1.1.0",
"nodemon": "^3.1.10",
"sass": "^1.89.2"
}
}

View File

@ -1,77 +0,0 @@
html,
body{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body{
padding-top: 80px;
padding-bottom: 80px;
background: #eee;
}
textarea{
max-width: 100%;
min-width: 100%;
height: 100px;
}
.container{
max-width: 1024px;
}
#main-nav{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 64px;
background: teal;
color: white;
}
#main-nav ul{
list-style: none;
display: inline-block;
}
#main-nav h4{
display: inline-block;
margin-left: 25px;
line-height: 64px;
}
#main-nav ul li{
display: inline-block;
margin-left: 15px;
}
#main-nav a{
color: white;
font-size: 2.25rem;
line-height: 64px;
transition: all 200ms ease-in-out;
}
#main-nav a:hover{
color: #eee;
font-size: 2.5rem;
}
#main-content{
padding: 14px 20px;
background: white;
border-radius: 8px;
}
#main-footer{
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
}
#main-footer p{
margin-bottom: 5px;
text-align: center;
}

21
src/models/benchmark.js Normal file
View File

@ -0,0 +1,21 @@
const { Sequelize } = require("sequelize");
module.exports = (sequelize) => {
const Benchmark = sequelize.define('Benchmark', {
name: {
type: Sequelize.STRING,
null: false,
},
description: {
type: Sequelize.TEXT,
},
scoring: {
type: Sequelize.STRING,
null: false,
},
},
{
tableName: 'benchmarks'
});
return Benchmark;
};

18
src/models/hardware.js Normal file
View File

@ -0,0 +1,18 @@
const { Sequelize } = require("sequelize");
module.exports = (sequelize) => {
const Hardware = sequelize.define('Hardware', {
name: {
type: Sequelize.STRING,
null: false,
},
type: {
type: Sequelize.STRING,
null: false,
},
},
{
tableName: 'hardware'
});
return Hardware;
};

View File

@ -5,6 +5,25 @@ const sequelize = new Sequelize({
storage: 'data/leviathan.db'
});
const Project = require('./project')(sequelize);
const Hardware = require('./hardware')(sequelize);
const Benchmark = require('./benchmark')(sequelize);
const Test = require('./test')(sequelize);
const Result = require('./result')(sequelize);
// Hardware/Test one-to-many
Hardware.hasMany(Test);
Test.belongsTo(Hardware);
// Benchmark/Test many-to-many
Benchmark.belongsToMany(Test, { through: 'tests_benchmarks' });
Test.belongsToMany(Benchmark, { through: 'tests_benchmarks' });
// Result/Benchmark many-to-one
Result.belongsTo(Benchmark);
Benchmark.hasMany(Result);
// Result/Test many-to-one
Result.belongsTo(Test);
Test.hasMany(Result);
module.exports = sequelize;

View File

@ -1,15 +0,0 @@
const { Sequelize } = require("sequelize");
module.exports = (sequelize) => {
const Project = sequelize.define('Project', {
title: {
type: Sequelize.STRING,
null: false
},
description: {
type: Sequelize.TEXT,
},
},
{});
return Project;
};

22
src/models/result.js Normal file
View File

@ -0,0 +1,22 @@
const { Sequelize } = require("sequelize");
module.exports = (sequelize) => {
const Result = sequelize.define('Result', {
avgScore: {
type: Sequelize.DOUBLE,
null: false,
},
minScore: {
type: Sequelize.DOUBLE,
null: true,
},
maxScore: {
type: Sequelize.DOUBLE,
null: true,
}
},
{
tableName: 'results'
});
return Result;
};

18
src/models/test.js Normal file
View File

@ -0,0 +1,18 @@
const { Sequelize } = require("sequelize");
module.exports = (sequelize) => {
const Test = sequelize.define('Test', {
title: {
type: Sequelize.STRING,
null: false,
},
description: {
type: Sequelize.STRING,
null: true,
}
},
{
tableName: 'tests'
});
return Test;
};

63
src/routes/api_v1.js Normal file
View File

@ -0,0 +1,63 @@
const Benchmark = require('../models').models.Benchmark;
const Result = require('../models').models.Result;
const Test = require('../models').models.Test;
// GET /api/v1/benchmark/details
exports.getBenchmarkDetails = async function(req, res) {
try {
const benchmark = await Benchmark.findByPk(req.query.benchmark_id);
if (!benchmark) {
return res.status(404).json({
error: 'Benchmark not found.'
})
}
res.json(benchmark);
} catch (err) {
res.status(500).json({
error: 'Internal server error occurred while fetching benchmark details.'
});
}
};
// GET /api/v1/result/list - list of results for a test
exports.getResultList = async function(req, res) {
try {
var results = await Result.findAll({
where: {
TestId: req.query.test_id,
BenchmarkId: req.query.benchmark_id
}
});
res.json(results);
} catch (err) {
res.status(500).json({
error: 'Internal server error occurred while fetching benchmark results.'
});
}
};
// POST /api/v1/result/add - add a benchmark result to the database
exports.postResultAdd = async function(req, res) {
try {
const result = await Result.create({
avgScore: req.body.result_avg,
minScore: req.body.result_min,
maxScore: req.body.result_max
});
let benchmark = await Benchmark.findByPk(req.body.result_benchmark);
result.setBenchmark(benchmark);
let test = await Test.findByPk(req.body.result_test);
result.setTest(test);
res.json('success');
} catch (err) {
res.status(500).json({
error: 'Internal server error occurred while adding result.'
});
}
};

42
src/routes/benchmark.js Normal file
View File

@ -0,0 +1,42 @@
const Benchmark = require('../models').models.Benchmark;
// GET /benchmark - redirects to benchmark list
exports.getIndex = async function(req, res) {
res.redirect('/benchmark/list');
};
// GET /benchmark/list - list of benchmarks
exports.getList = async function(req, res) {
var benchmarks = await Benchmark.findAll();
res.render('benchmark/list', {
benchmarks: benchmarks
});
};
// GET /benchmark/:benchmark_id - view information about a benchmark
exports.getView = async function(req, res) {
var benchmark = await Benchmark.findAll({
where: {
id: req.params.benchmark_id
}
});
res.render('benchmark/view', {
benchmark: benchmark[0]
});
};
// GET /benchmark/add - add a new benchmark
exports.getAdd = async function(req, res) {
res.render('benchmark/add');
};
// POST /benchmark/add - add the benchmark to the database
exports.postAdd = async function(req, res) {
var benchmark = await Benchmark.create({
name: req.body.benchmark_name,
scoring: req.body.benchmark_scoring,
description: req.body.benchmark_description
});
res.redirect('/benchmark');
};

41
src/routes/hardware.js Normal file
View File

@ -0,0 +1,41 @@
const Hardware = require('../models').models.Hardware;
// GET /hardware - redirects to project list
exports.getIndex = async function(req, res) {
res.redirect('/hardware/list');
};
// GET /hardware/list - list of hardware
exports.getList = async function(req, res) {
var hardware = await Hardware.findAll();
res.render('hardware/list', {
hardware: hardware
});
};
// GET /hardware/:hardware_id - view information about a piece of hardware
exports.getView = async function(req, res) {
var hardware = await Hardware.findAll({
where: {
id: req.params.hardware_id
}
});
res.render('hardware/view', {
hardware: hardware[0]
});
};
// GET /hardware/add - add a new hardware
exports.getAdd = async function(req, res) {
res.render('hardware/add');
};
// POST /hardware/add - add the hardware to the database
exports.postAdd = async function(req, res) {
var hardware = await Hardware.create({
name: req.body.hardware_name,
type: req.body.hardware_type
});
res.redirect('/hardware');
};

View File

@ -1,9 +1,39 @@
const Project = require('../models').models.Project;
// load routes
const topRoutes = require('./toplevel');
const testRoutes = require('./test');
const hardwareRoutes = require('./hardware');
const benchmarkRoutes = require('./benchmark');
const apiv1Routes = require('./api_v1');
module.exports = function(app) {
// top-level routes
app.get('/', topRoutes.getIndex);
// hardware routes
app.get('/hardware', hardwareRoutes.getIndex);
app.get('/hardware/list', hardwareRoutes.getList);
app.get('/hardware/add', hardwareRoutes.getAdd);
app.post('/hardware/add', hardwareRoutes.postAdd);
app.get('/hardware/:hardware_id', hardwareRoutes.getView);
// benchmark routes
app.get('/benchmark', benchmarkRoutes.getIndex);
app.get('/benchmark/list', benchmarkRoutes.getList);
app.get('/benchmark/add', benchmarkRoutes.getAdd);
app.post('/benchmark/add', benchmarkRoutes.postAdd);
app.get('/benchmark/:benchmark_id', benchmarkRoutes.getView);
// test routes
app.get('/test', testRoutes.getIndex);
app.get('/test/list', testRoutes.getList);
app.get('/test/add', testRoutes.getAdd);
app.post('/test/add', testRoutes.postAdd);
app.get('/test/:test_id', testRoutes.getView);
// API v1 routes
app.get('/api/v1/benchmark/details', apiv1Routes.getBenchmarkDetails);
app.get('/api/v1/result/list', apiv1Routes.getResultList);
app.post('/api/v1/result/add', apiv1Routes.postResultAdd);
// GET / - primary app dashboard
exports.getIndex = async function(req, res) {
var projects = await Project.findAll();
res.render('index/dashboard', {
projects: projects
});
};

View File

@ -1,41 +0,0 @@
const Project = require('../models').models.Project;
// GET /project - redirects to project list
exports.getIndex = async function(req, res) {
res.redirect('/project/list');
};
// GET /project/list - list of projects
exports.getList = async function(req, res) {
var projects = await Project.findAll();
res.render('project/list', {
projects: projects
});
};
// GET /project/:project_id - view information about a project
exports.getView = async function(req, res) {
var project = await Project.findAll({
where: {
id: req.params.project_id
}
});
res.render('project/view', {
project: project[0]
});
};
// GET /project/add - add a new project
exports.getAdd = async function(req, res) {
res.render('project/add');
};
// POST /project/add - add the project to the database
exports.postAdd = async function(req, res) {
var project = await Project.create({
title: req.body.project_title,
description: req.body.project_description
});
res.redirect('/project');
};

59
src/routes/test.js Normal file
View File

@ -0,0 +1,59 @@
const Test = require('../models').models.Test;
const Hardware = require('../models').models.Hardware;
const Benchmark = require('../models').models.Benchmark;
// GET /test - redirects to test list
exports.getIndex = async function(req, res) {
res.redirect('/test/list');
};
// GET /test/list - list of tests
exports.getList = async function(req, res) {
var tests = await Test.findAll({order: [['updatedAt', 'DESC']]});
res.render('test/list', {
tests: tests
});
};
// GET /test/:test_id - view information about a test
exports.getView = async function(req, res) {
var test = await Test.findAll({
where: {
id: req.params.test_id
}
});
res.render('test/view', {
test: test[0]
});
};
// GET /test/add - add a new test
exports.getAdd = async function(req, res) {
var hardware = await Hardware.findAll();
var benchmarks = await Benchmark.findAll();
res.render('test/add', {
hardware: hardware,
benchmarks: benchmarks
});
};
// POST /test/add - add the test to the database
exports.postAdd = async function(req, res) {
var test = await Test.create({
title: req.body.test_title,
description: req.body.test_description,
});
// add link to hardware
let hardware = await Hardware.findByPk(req.body.test_hardware);
test.setHardware(hardware);
// add links to benchmarks
for (let b = 0; b < req.body.test_benchmarks.length; b++) {
let benchmark = await Benchmark.findByPk(req.body.test_benchmarks[b]);
test.addBenchmark(benchmark);
}
res.redirect('/test/' + test.id);
};

9
src/routes/toplevel.js Normal file
View File

@ -0,0 +1,9 @@
const Test = require('../models').models.Test;
// GET / - primary app dashboard
exports.getIndex = async function(req, res) {
var tests = await Test.findAll();
res.render('index/dashboard', {
tests: tests
});
};

40
views/benchmark/add.twig Normal file
View File

@ -0,0 +1,40 @@
{% extends 'layouts/default.twig' %}
{% block title %}Add a Benchmark{% endblock %}
{% block content %}
<div class="row">
<h2>Add a benchmark</h2>
<form class="twelve columns" action="/benchmark/add" method="POST">
<div class="row">
<div class="nine columns">
<label for="benchmark_name">
Benchmark name:
<input id="benchmark_name" class="u-full-width" type="text" name="benchmark_name" placeholder="My hardware benchmarking benchmark">
</label>
</div>
<div class="three columns">
<label for="benchmark_scoring">
Scoring type:
<select id="benchmark_scoring" class="u-full-width" name="benchmark_scoring">
<option value="fps">Frames per second</option>
<option value="pts">Total points</option>
<option value="time">Frame time</option>
</select>
</label>
</div>
</div>
<div class="row">
<label for="benchmark_description">
Benchmark description:
<textarea id="benchmark_description" class="twelve columns" cols="30" rows="10" name="benchmark_description"></textarea>
</label>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
{% endblock %}

31
views/benchmark/list.twig Normal file
View File

@ -0,0 +1,31 @@
{% extends 'layouts/default.twig' %}
{% block title %}List of Benchmarks{% endblock %}
{% block content %}
<div class="row">
<h2>Benchmarks</h2>
<a href="/benchmark/add">Add a benchmark</a>
<table class="twelve columns">
<thead>
<tr>
<td>Benchmark name</td>
<td>Scoring type</td>
<td>Created at</td>
<td>Last updated</td>
</tr>
</thead>
<tbody>
{% for b in benchmarks %}
<tr>
<td><a href="/benchmark/{{ b.id }}">{{ b.name }}</a></td>
<td>{{ b.scoring }}</td>
<td>{{ b.createdAt | date('m/d/Y g:ia') }}</td>
<td>{{ b.updatedAt | date('m/d/Y g:ia') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

42
views/benchmark/view.twig Normal file
View File

@ -0,0 +1,42 @@
{% extends 'layouts/default.twig' %}
{% block title %}{{ benchmark.name }}{% endblock %}
{% block content %}
<div class="row">
<h2>{{ benchmark.name }}</h2>
<p>Scoring type: {{ benchmark.scoring }}</p>
<p>{{ benchmark.description }}</p>
<hr>
<h2>Recently Updated Tests Using This Benchmark:</h2>
{% if benchmark.getTests().length > 0 %}
<table class="twelve columns">
<thead>
<tr>
<th>Test Name</th>
<th>Hardware Tested</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
{% for t in benchmark.getTests() %}
<tr>
<td><a href="/test/{{ t.id }}">{{ t.title }}</a></td>
<td>{{ t.getHardware().name }}</td>
<td>{{ t.updatedAt | date('m/d/Y g:ia') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>There are no tests registered yet for this hardware.</p>
{% endif %}
<p><a href="/benchmark">Back</a></p>
</div>
{% endblock %}

35
views/hardware/add.twig Normal file
View File

@ -0,0 +1,35 @@
{% extends 'layouts/default.twig' %}
{% block title %}Add Hardware{% endblock %}
{% block content %}
<div class="row">
<h2>Add hardware</h2>
<form class="twelve columns" action="/hardware/add" method="POST">
<div class="row">
<div class="nine columns">
<label for="hardware_name">
Hardware name:
<input id="hardware_name" class="u-full-width" type="text" name="hardware_name" placeholder="EVGA RTX 3080 Ti">
</label>
</div>
<div class="three columns">
<label for="hardware_type">
Hardware type:
<select id="hardware_type" class="u-full-width" name="hardware_type">
<option value="cpu">Processor</option>
<option value="mem">Memory</option>
<option value="gpu">Graphics card</option>
<option value="ssd">SSD</option>
<option value="hdd">HDD</option>
</select>
</label>
</div>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
{% endblock %}

View File

@ -1,24 +1,26 @@
{% extends 'layouts/default.twig' %}
{% block title %}List of Projects{% endblock %}
{% block title %}List of Hardware{% endblock %}
{% block content %}
<div class="row">
<h2>Projects</h2>
<a href="/project/add">Add a project</a>
<h2>Hardware</h2>
<a href="/hardware/add">Add new hardware</a>
<table class="twelve columns">
<thead>
<tr>
<td>Title</td>
<td>Name</td>
<td>Type</td>
<td>Created at</td>
<td>Last updated</td>
</tr>
</thead>
<tbody>
{% for p in projects %}
{% for h in hardware %}
<tr>
<td><a href="/project/{{ p.id }}">{{ p.title }}</a></td>
<td><a href="/hardware/{{ h.id }}">{{ h.name }}</a></td>
<td>{{ h.type }}</td>
<td>{{ p.createdAt | date('m/d/Y g:ia') }}</td>
<td>{{ p.updatedAt | date('m/d/Y g:ia') }}</td>
</tr>

42
views/hardware/view.twig Normal file
View File

@ -0,0 +1,42 @@
{% extends 'layouts/default.twig' %}
{% block title %}{{ hardware.name }}{% endblock %}
{% block content %}
<div class="row">
<h2>{{ hardware.name }}</h2>
<p>Hardware type: {{ hardware.type }}</p>
<hr>
<h2>Recently Updated Tests for This Hardware:</h2>
{% if hardware.getTests().length > 0 %}
<table class="twelve columns">
<thead>
<tr>
<th>Test Name</th>
<th># Benchmarks</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
{% for t in hardware.getTests() %}
<tr>
<td><a href="/test/{{ t.id }}">{{ t.title }}</a></td>
<td>{{ t.getBenchmarks().length }}</td>
<td>{{ t.updatedAt | date('m/d/Y g:ia') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>There are no tests registered yet for this hardware.</p>
{% endif %}
</section>
<p><a href="/hardware">Back</a></p>
</div>
{% endblock %}

View File

@ -4,41 +4,24 @@
{% block content %}
<div class="row">
<h2>Recently updated projects:</h2>
<h2>Recently updated tests:</h2>
<table class="twelve columns">
<thead>
<tr>
<td>Title</td>
<td>Last updated</td>
<td>Created at</td>
</tr>
</thead>
<tbody>
{% for p in projects %}
{% for t in tests %}
<tr>
<td>{{ p.title }}</td>
<td>{{ p.updatedAt | date('m/d/Y g:ia') }}</td>
<td><a href="/test/{{ t.id }}">{{ t.title }}</a></td>
<td>{{ t.updatedAt | date('m/d/Y g:ia') }}</td>
<td>{{ t.createdAt | date('m/d/Y g:ia') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<hr>
<div class="row">
<h2>Recently added results:</h2>
<table class="twelve columns">
<thead>
<tr>
<td>Benchmark</td>
<td>Hardware</td>
<td>Average/Score</td>
<td>Scoring type</td>
<td>Added on</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% endblock %}

View File

@ -5,9 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block title %}{% endblock %} | Leviathan</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/eye.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/js/bootstrap.min.js" charset="utf-8"></script>
<script src="/js/scar.js"></script>
{% block scripts %}{% endblock %}
</head>
<body>
{% include 'partials/navbar.twig' %}

View File

@ -1,11 +1,29 @@
<nav id="main-nav">
<div class="nav-left">
<h4>Leviathan</h4>
<ul>
<li><a href="/">Dashboard</a></li>
<li><a href="/project">Projects</a></li>
<li><a href="/hardware">Hardware</a></li>
<li><a href="/benchmark">Benchmarks</a></li>
<nav id="main-nav" class="navbar navbar-expand-md bg-body-secondary mb-3">
<div class="container-fluid">
<a class="navbar-brand mb-0 h1" href="#">Leviathan</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link" href="/">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/benchmark">Benchmarks</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/hardware">Hardware</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/test">Test</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/report">Reports</a>
</li>
</ul>
</div>
</div>
</nav>

View File

@ -1,27 +0,0 @@
{% extends 'layouts/default.twig' %}
{% block title %}Add a Project{% endblock %}
{% block content %}
<div class="row">
<h2>Add a project</h2>
<form class="twelve columns" action="/project/add" method="POST">
<div class="row">
<label for="project_title">
Project title:
<input id="project_title" class="u-full-width" type="text" name="project_title" placeholder="My hardware benchmarking project">
</label>
</div>
<div class="row">
<label for="project_description">
Project description:
<textarea id="project_description" class="twelve columns" cols="30" rows="10" name="project_description"></textarea>
</label>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% extends 'layouts/default.twig' %}
{% block title %}{{ project.title }}{% endblock %}
{% block content %}
<div class="row">
<h2>{{ project.title }}</h2>
<p>{{ project.description }}</p>
<hr>
<p><a href="/project">Back</a></p>
</div>
{% endblock %}

53
views/test/add.twig Normal file
View File

@ -0,0 +1,53 @@
{% extends 'layouts/default.twig' %}
{% block title %}Add a Test{% endblock %}
{% block content %}
<div class="row">
<h2>Add a test</h2>
<form class="twelve columns" action="/test/add" method="POST">
<div class="row">
<div class="three columns">
<label for="test_title">
Test title:
<input id="test_title" class="u-full-width" type="text" name="test_title" placeholder="My test">
</label>
</div>
<div class="nine columns">
<label for="test_hardware">
Test hardware:
<select id="test_hardware" class="u-full-width" name="test_hardware">
{% for h in hardware %}
<option value="{{ h.id }}">{{ h.name }}</option>
{% endfor %}
</select>
</label>
</div>
</div>
<div class="row">
<div class="twelve columns">
<label for="test_benchmarks">
Test benchmarks:
<select id="test_benchmarks" class="u-full-width" name="test_benchmarks[]" multiple>
{% for b in benchmarks %}
<option value="{{ b.id }}">{{ b.name }}</option>
{% endfor %}
</select>
</label>
</div>
</div>
<div class="row">
<label for="test_description">
Test description:
<textarea id="test_description" class="twelve columns" cols="30" rows="10" name="test_description"></textarea>
</label>
</div>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
{% endblock %}

29
views/test/list.twig Normal file
View File

@ -0,0 +1,29 @@
{% extends 'layouts/default.twig' %}
{% block title %}List of Tests{% endblock %}
{% block content %}
<div class="row">
<h2>Tests</h2>
<a href="/test/add">Add a test</a>
<table class="twelve columns">
<thead>
<tr>
<td>Title</td>
<td>Created at</td>
<td>Last updated</td>
</tr>
</thead>
<tbody>
{% for t in tests %}
<tr>
<td><a href="/test/{{ t.id }}">{{ t.title }}</a></td>
<td>{{ t.createdAt | date('m/d/Y g:ia') }}</td>
<td>{{ t.updatedAt | date('m/d/Y g:ia') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

84
views/test/view.twig Normal file
View File

@ -0,0 +1,84 @@
{% extends 'layouts/default.twig' %}
{% block title %}Test: {{ test.title }}{% endblock %}
{% block scripts %}
<script src="/js/test.js" type="text/javascript"></script>
{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<h2>Test: {{ test.title }}</h2>
<h4>Hardware tested: <a href="/hardware/{{ test.getHardware().id }}">{{ test.getHardware().name }}</a></h4>
</div>
</div>
<hr>
<div class="row">
<form id="test-result-form" class="col-12" action="/api/v1/result/add" method="post">
<input type="hidden" name="result_test" value="{{ test.id }}">
<div class="row">
<div class="col-4">
<label class="form-label" for="result_benchmark">Benchmark:</label>
<select id="result_benchmark" class="form-select" name="result_benchmark">
{% for b in test.getBenchmarks() %}
<option value="{{ b.id }}">{{ b.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-2">
<label class="form-label" for="result_avg">Average:</label>
<input id="result_avg" class="form-control" type="number" name="result_avg" step="0.01" required>
</div>
<div class="col-2">
<label class="form-label" for="result_min">Minimum:</label>
<input id="result_min" class="form-control" type="number" name="result_min" step="0.01">
</div>
<div class="col-2">
<label class="form-label" for="result_max">Maximum:</label>
<input id="result_max" class="form-control" type="number" name="result_max" step="0.01">
</div>
<div class="col-2">
<input type="submit" class="btn btn-primary" value="Add Result">
</div>
</div>
</form>
</div>
<hr>
<div class="row">
<div class="col-12">
<h3 class="mb-3">Benchmarks</h3>
<table id="results-table" class="table table-hover table-responsive" data-test-id="{{ test.id }}">
<thead class="table-light">
<tr>
<td>Benchmark</td>
<td>Scoring type</td>
<td># Results</td>
<td>Avg.</td>
<td>Min.</td>
<td>Max.</td>
</tr>
</thead>
<tbody>
{% for b in test.getBenchmarks() %}
<tr data-benchmark-id="{{ b.id }}"></tr>
{% endfor %}
</tbody>
</table>
</div>
<hr>
<p><a href="/test">Back</a></p>
</div>
{% endblock %}