- fix graph rendering issues & add server-side logging
This commit is contained in:
parent
94ccd14689
commit
2050c715dc
8 changed files with 100 additions and 32 deletions
27
package-lock.json
generated
27
package-lock.json
generated
|
@ -17,6 +17,7 @@
|
||||||
"d3-array": "^3.2.4",
|
"d3-array": "^3.2.4",
|
||||||
"d3-scale": "^4.0.2",
|
"d3-scale": "^4.0.2",
|
||||||
"d3-shape": "^3.2.0",
|
"d3-shape": "^3.2.0",
|
||||||
|
"debug": "^4.4.0",
|
||||||
"drizzle-orm": "^0.36.4",
|
"drizzle-orm": "^0.36.4",
|
||||||
"layerchart": "^0.59.1",
|
"layerchart": "^0.59.1",
|
||||||
"mysql2": "^3.11.4"
|
"mysql2": "^3.11.4"
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
"@types/d3-array": "^3.2.1",
|
"@types/d3-array": "^3.2.1",
|
||||||
"@types/d3-scale": "^4.0.8",
|
"@types/d3-scale": "^4.0.8",
|
||||||
"@types/d3-shape": "^3.1.6",
|
"@types/d3-shape": "^3.1.6",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"drizzle-kit": "^0.28.1",
|
"drizzle-kit": "^0.28.1",
|
||||||
|
@ -2364,6 +2366,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/debug": {
|
||||||
|
"version": "4.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
|
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
|
@ -2388,6 +2400,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "0.7.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
|
||||||
|
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.9.0",
|
"version": "22.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
||||||
|
@ -3425,10 +3444,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
|
@ -5347,7 +5365,6 @@
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mysql2": {
|
"node_modules/mysql2": {
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
"@types/d3-array": "^3.2.1",
|
"@types/d3-array": "^3.2.1",
|
||||||
"@types/d3-scale": "^4.0.8",
|
"@types/d3-scale": "^4.0.8",
|
||||||
"@types/d3-shape": "^3.1.6",
|
"@types/d3-shape": "^3.1.6",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"drizzle-kit": "^0.28.1",
|
"drizzle-kit": "^0.28.1",
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
"d3-array": "^3.2.4",
|
"d3-array": "^3.2.4",
|
||||||
"d3-scale": "^4.0.2",
|
"d3-scale": "^4.0.2",
|
||||||
"d3-shape": "^3.2.0",
|
"d3-shape": "^3.2.0",
|
||||||
|
"debug": "^4.4.0",
|
||||||
"drizzle-orm": "^0.36.4",
|
"drizzle-orm": "^0.36.4",
|
||||||
"layerchart": "^0.59.1",
|
"layerchart": "^0.59.1",
|
||||||
"mysql2": "^3.11.4"
|
"mysql2": "^3.11.4"
|
||||||
|
|
|
@ -2,10 +2,14 @@ import type { Handle } from "@sveltejs/kit";
|
||||||
|
|
||||||
import { deleteSessionTokenCookie, setSessionTokenCookie, validateSession } from "./lib/session/session";
|
import { deleteSessionTokenCookie, setSessionTokenCookie, validateSession } from "./lib/session/session";
|
||||||
|
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
const log = debug('hooks');
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
const token = event.cookies.get("session") ?? null;
|
const token = event.cookies.get("session") ?? null;
|
||||||
if (token === null) {
|
if (token === null) {
|
||||||
console.log("No session available")
|
log("No session available")
|
||||||
event.locals.userId = null;
|
event.locals.userId = null;
|
||||||
event.locals.session = null;
|
event.locals.session = null;
|
||||||
return resolve(event);
|
return resolve(event);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { scaleBand } from 'd3-scale';
|
import { scaleBand } from 'd3-scale';
|
||||||
import { curveLinearClosed } from 'd3-shape';
|
import { curveLinear, curveLinearClosed } from 'd3-shape';
|
||||||
import { flatGroup } from 'd3-array';
|
import { flatGroup } from 'd3-array';
|
||||||
import { Axis, Chart, Points, Spline, Svg } from 'layerchart';
|
import { Axis, Chart, Points, Spline, Svg } from 'layerchart';
|
||||||
|
|
||||||
|
@ -10,23 +10,29 @@
|
||||||
data: { skill: string; participant: number; rating: number | undefined }[];
|
data: { skill: string; participant: number; rating: number | undefined }[];
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const averageValues = flatGroup(data, (d) => d.skill).map(([skill, ratings]) => {
|
const averageValues = flatGroup(data, (d) => d.skill)
|
||||||
|
.map(([skill, ratings]) => {
|
||||||
const actualRatings = ratings.filter((r) => r.rating !== undefined);
|
const actualRatings = ratings.filter((r) => r.rating !== undefined);
|
||||||
const avg = actualRatings.reduce((a, b) => a + (b.rating ?? 0), 0) / actualRatings.length;
|
const avg = actualRatings.reduce((a, b) => a + (b.rating ?? 0), 0) / actualRatings.length;
|
||||||
return { skill, rating: avg };
|
return { skill, rating: avg };
|
||||||
});
|
})
|
||||||
|
// .filter((d) => !isNaN(d.rating));
|
||||||
|
.map((d) => ({
|
||||||
|
...d,
|
||||||
|
rating: isNaN(d.rating) ? undefined : d.rating
|
||||||
|
}));
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
'stroke-slate-300',
|
{ stroke: 'stroke-slate-300', fill: 'fill-slate-400' },
|
||||||
'stroke-violet-300',
|
{ stroke: 'stroke-violet-300', fill: 'fill-violet-400' },
|
||||||
'stroke-red-300',
|
{ stroke: 'stroke-red-300', fill: 'fill-red-400' },
|
||||||
'stroke-lime-300',
|
{ stroke: 'stroke-lime-300', fill: 'fill-lime-400' },
|
||||||
'stroke-blue-300',
|
{ stroke: 'stroke-blue-300', fill: 'fill-blue-400' },
|
||||||
'stroke-amber-300',
|
{ stroke: 'stroke-amber-300', fill: 'fill-amber-400' },
|
||||||
'stroke-stone-300',
|
{ stroke: 'stroke-stone-300', fill: 'fill-stone-400' },
|
||||||
'stroke-emerald-300',
|
{ stroke: 'stroke-emerald-300', fill: 'fill-emerald-400' },
|
||||||
'stroke-cyan-300',
|
{ stroke: 'stroke-cyan-300', fill: 'fill-cyan-400' },
|
||||||
'stroke-pink-300'
|
{ stroke: 'stroke-pink-300', fill: 'fill-pink-400' }
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -51,13 +57,22 @@
|
||||||
<Axis placement="angle" grid={{ class: 'stroke-slate-950' }} />
|
<Axis placement="angle" grid={{ class: 'stroke-slate-950' }} />
|
||||||
{#each flatGroup(data, (d) => d.participant) as [participant, ratings], idx}
|
{#each flatGroup(data, (d) => d.participant) as [participant, ratings], idx}
|
||||||
<Spline
|
<Spline
|
||||||
|
data={[...ratings, ratings[0]]}
|
||||||
|
curve={curveLinear}
|
||||||
|
class="{colors[idx % colors.length].stroke} stroke-[2px]"
|
||||||
|
/>
|
||||||
|
<Points
|
||||||
data={ratings}
|
data={ratings}
|
||||||
curve={curveLinearClosed}
|
class="{colors[idx % colors.length].stroke} {colors[idx % colors.length]
|
||||||
class="{colors[idx % colors.length]} stroke-[2px]"
|
.fill} stroke-[2px]"
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
<Spline data={averageValues} curve={curveLinearClosed} class="stroke-lime-600 stroke-[4px]" />
|
<Spline
|
||||||
<!-- <Points class="fill-red-900 stroke-slate-950" /> -->
|
data={[...averageValues, averageValues[0]]}
|
||||||
|
curve={curveLinear}
|
||||||
|
class="stroke-lime-600 stroke-[4px]"
|
||||||
|
/>
|
||||||
|
<Points data={averageValues} class="fill-lime-500 stroke-lime-600 stroke-[2px]" />
|
||||||
</Svg>
|
</Svg>
|
||||||
</Chart>
|
</Chart>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,7 +22,7 @@ export async function loadSurvey(id: number, ownerId: number) {
|
||||||
accessToken: participant.accessToken,
|
accessToken: participant.accessToken,
|
||||||
answers: answers.filter(answer => answer.participantId === participant.id).map(answer => ({
|
answers: answers.filter(answer => answer.participantId === participant.id).map(answer => ({
|
||||||
skillId: answer.skillId,
|
skillId: answer.skillId,
|
||||||
rating: answer.rating
|
rating: answer.rating / 10 // convert back to original vallue. The DB stores integer values only to prevent rounding and matching errors
|
||||||
}))
|
}))
|
||||||
})),
|
})),
|
||||||
skills: skills.map(skill => ({
|
skills: skills.map(skill => ({
|
||||||
|
|
|
@ -2,18 +2,25 @@ import { error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { loadSurvey } from '$lib/queries';
|
import { loadSurvey } from '$lib/queries';
|
||||||
|
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
const log = debug('survey:admin');
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, locals }) => {
|
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||||
const surveyId = parseInt(params.surveyId);
|
const surveyId = parseInt(params.surveyId);
|
||||||
|
|
||||||
if (isNaN(surveyId)) {
|
if (isNaN(surveyId)) {
|
||||||
|
log('Invalid survey ID %s', params.surveyId);
|
||||||
error(400, 'Invalid survey ID');
|
error(400, 'Invalid survey ID');
|
||||||
}
|
}
|
||||||
if (!locals.userId) {
|
if (!locals.userId) {
|
||||||
|
log('User is not logged in');
|
||||||
error(403, 'User is not logged in');
|
error(403, 'User is not logged in');
|
||||||
}
|
}
|
||||||
|
|
||||||
const surveyData = await loadSurvey(surveyId, locals.userId);
|
const surveyData = await loadSurvey(surveyId, locals.userId);
|
||||||
if (!surveyData) {
|
if (!surveyData) {
|
||||||
|
log('Survey not found or user (%s) does not have access: %s', locals.userId, params.surveyId);
|
||||||
error(404, 'Survey not found');
|
error(404, 'Survey not found');
|
||||||
}
|
}
|
||||||
return surveyData;
|
return surveyData;
|
||||||
|
|
|
@ -5,6 +5,11 @@ import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
const log_load = debug('survey:load');
|
||||||
|
const log_store = debug('survey:store');
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, url }) => {
|
export const load: PageServerLoad = async ({ params, url }) => {
|
||||||
const results = await db.select()
|
const results = await db.select()
|
||||||
.from(surveyAccessTable)
|
.from(surveyAccessTable)
|
||||||
|
@ -13,10 +18,12 @@ export const load: PageServerLoad = async ({ params, url }) => {
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
|
log_load('Survey not found: %s', params.accessToken);
|
||||||
error(404, 'Survey not found');
|
error(404, 'Survey not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
|
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
|
||||||
|
log_load('Answers already submitted: %s', params.accessToken);
|
||||||
error(400, 'Answers already submitted');
|
error(400, 'Answers already submitted');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +51,12 @@ export const actions = {
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
|
log_store('Survey not found: %s', params.accessToken);
|
||||||
error(404, 'Survey not found');
|
error(404, 'Survey not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
|
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
|
||||||
|
log_store('Answers already submitted: %s', params.accessToken);
|
||||||
error(400, 'Answers already submitted');
|
error(400, 'Answers already submitted');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,20 +65,29 @@ export const actions = {
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
// validate that the form doesn't contain invalid skill IDs
|
// validate that the form doesn't contain invalid skill IDs
|
||||||
const skillEntries = [...formData.entries()].filter(([key, _]) => key.startsWith('disable-'));
|
const skillEntries = [...formData.entries()].filter(([key, _]) => !key.startsWith('disable-'));
|
||||||
const allIdsValid = skillEntries.every(([key, _]) => skills.some(skill => skill.id.toString() === key));
|
const allIdsValid = skillEntries.every(([key, _]) => skills.some(skill => skill.id.toString() === key));
|
||||||
if (!allIdsValid || skillEntries.length !== skills.length) {
|
const deselectedIds = [...formData.entries().filter(([key, _]) => key.startsWith('disable-')).map(([key, _]) => parseInt(key.replace('disable-', '')))];
|
||||||
|
if (!allIdsValid || skillEntries.length + deselectedIds.length !== skills.length) {
|
||||||
|
let missing_ids = skills.filter(skill => !skillEntries.some(([key, _]) => key === skill.id.toString()) && !deselectedIds.includes(skill.id));
|
||||||
|
let invalid_ids = skillEntries.filter(([key, _]) => !skills.some(skill => skill.id.toString() === key)).map(([key, _]) => key);
|
||||||
|
log_store.log("Invalid (%o) or missing (%o) skill IDs", invalid_ids, missing_ids);
|
||||||
error(400, 'Invalid skill ID');
|
error(400, 'Invalid skill ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
const answers = skillEntries.map(([key, value]) => ({
|
const answers = skillEntries.map(([key, value]) => ({
|
||||||
participantId: results[0].survey_access_table.id,
|
participantId: results[0].survey_access_table.id,
|
||||||
skillId: parseInt(key),
|
skillId: parseInt(key),
|
||||||
rating: parseFloat(value.toString())
|
rating: parseFloat(value.toString()) * 10 // convert to int to store in the database without rounding issues etc.
|
||||||
}));
|
}));
|
||||||
if (answers.some(answer => isNaN(answer.rating) || answer.rating < 0 || answer.rating > 3)) {
|
if (answers.some(answer => isNaN(answer.rating) || answer.rating < 0 || answer.rating > 30)) {
|
||||||
|
log_store(
|
||||||
|
"Invalid answer: %o",
|
||||||
|
answers.filter(answer => isNaN(answer.rating) || answer.rating < 0 || answer.rating > 30)
|
||||||
|
);
|
||||||
error(400, 'Invalid answer');
|
error(400, 'Invalid answer');
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.insert(surveyAnswersTable).values([...answers]);
|
await db.insert(surveyAnswersTable).values([...answers]);
|
||||||
|
|
||||||
redirect(303, `/${params.accessToken}/thanks`);
|
redirect(303, `/${params.accessToken}/thanks`);
|
||||||
|
|
|
@ -8,17 +8,21 @@ import { error } from '@sveltejs/kit';
|
||||||
import { verify } from '@node-rs/argon2';
|
import { verify } from '@node-rs/argon2';
|
||||||
import { generateRandomToken } from '$lib/randomToken';
|
import { generateRandomToken } from '$lib/randomToken';
|
||||||
|
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
let log = debug('login');
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async (event) => {
|
default: async (event) => {
|
||||||
const formData = await event.request.formData();
|
const formData = await event.request.formData();
|
||||||
const email = formData.get('email')?.toString();
|
const email = formData.get('email')?.toString();
|
||||||
const password = formData.get('password')?.toString();
|
const password = formData.get('password')?.toString();
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
|
log('Email and password are required');
|
||||||
error(400, 'Email and password are required');
|
error(400, 'Email and password are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userCredentials = await db.select().from(usersTable).where(eq(usersTable.email, email));
|
const userCredentials = await db.select().from(usersTable).where(eq(usersTable.email, email));
|
||||||
console.log(userCredentials, formData.get('email'));
|
|
||||||
if (userCredentials.length === 0) {
|
if (userCredentials.length === 0) {
|
||||||
error(403, 'Invalid credentials');
|
error(403, 'Invalid credentials');
|
||||||
}
|
}
|
||||||
|
@ -38,6 +42,7 @@ export const actions = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
log('Invalid credentials for %s', email);
|
||||||
error(403, 'Invalid credentials');
|
error(403, 'Invalid credentials');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue