Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
57bf38d
save
thebeninator Aug 29, 2025
890a5b3
poll job status on print page; local storage first pass
thebeninator Aug 29, 2025
6add54c
delete printId from local storage on complete
thebeninator Aug 29, 2025
8f50098
resume polling if printId still exists
thebeninator Aug 30, 2025
d8179ce
add undefined check for localStorage in useEffect
thebeninator Aug 30, 2025
aad6860
save
thebeninator Sep 1, 2025
5403349
save
thebeninator Sep 1, 2025
de4e720
use promise for tryResolvePrint
thebeninator Sep 2, 2025
177fc20
convert /status to GET, require token
thebeninator Sep 8, 2025
60ad976
multiple print job tracking first pass
thebeninator Sep 8, 2025
9eb41ee
fix lint
thebeninator Sep 8, 2025
78d7f23
fix interval using stale jobs state
thebeninator Sep 8, 2025
837b980
implement doodoo ui for active prints
thebeninator Sep 9, 2025
eb764ad
store job in local immediately; remove guard clause for 'PRINTED' state
thebeninator Sep 10, 2025
ae994b7
some clean up
thebeninator Sep 10, 2025
0c0767a
FIX LINT
thebeninator Sep 10, 2025
2f15f17
check if localstorage printJobs exists first before setting state
thebeninator Sep 15, 2025
7faa2bc
undoodoo the doodoo ui first pass
thebeninator Sep 29, 2025
6ab42d1
implement page escrow
thebeninator Nov 15, 2025
85a6671
some cleanup; actually save escrow modify on print fail
thebeninator Nov 15, 2025
02b2748
failed/completed jobs notifs now persist for 5 secs
thebeninator Nov 18, 2025
abdf167
fix fail/complete notif not being destroyed after page refresh
thebeninator Nov 24, 2025
a4586c7
rename function
thebeninator Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/main_endpoints/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ const UserSchema = new Schema(
type: Number,
default: 0
},
escrowPagesPrinted: {
type: Number,
default: 0
},
apiKey: {
type: String,
default: ''
Expand Down
59 changes: 57 additions & 2 deletions api/main_endpoints/routes/Printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ const {
UNAUTHORIZED,
NOT_FOUND,
SERVER_ERROR,
BAD_REQUEST
} = require('../../util/constants').STATUS_CODES;
const {
PRINTING = {}
} = require('../../config/config.json');
const User = require('../models/User.js');

// see https://github.com/SCE-Development/Quasar/tree/dev/docker-compose.dev.yml#L11
let PRINTER_URL = process.env.PRINTER_URL
Expand Down Expand Up @@ -89,6 +91,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => {
return res.status(OK).send({ printId: null });
}

const user = await User.findById(decodedToken._id);
const dir = path.join(__dirname, 'printing');
const { totalChunks, chunkIdx } = req.body;

Expand All @@ -97,7 +100,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => {
return res.sendStatus(OK);
}

const { copies, sides, id } = req.body;
const { copies, sides, id, totalPages } = req.body;

const chunks = await fs.promises.readdir(dir);
const assembledPdfFromChunks = path.join(dir, id + '.pdf');
Expand All @@ -116,12 +119,17 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => {
}
}

const stream = await fs.createReadStream(assembledPdfFromChunks);
const stream = await fs.promises.readFile(assembledPdfFromChunks);
const data = new FormData();
data.append('file', stream, {filename: id, type: 'application/pdf'});
data.append('copies', copies);
data.append('sides', sides);

if (Number(totalPages) > 30 - user.pagesPrinted - user.escrowPagesPrinted) {
await cleanUpChunks(dir, id);
return res.sendStatus(BAD_REQUEST);
}

try {
// full pdf can be sent to quasar no problem
const printRes = await axios.post(PRINTER_URL + '/print', data, {
Expand All @@ -137,6 +145,9 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => {

await cleanUpChunks(dir, id);
res.status(OK).send(printId);

user.escrowPagesPrinted += Number(totalPages);
await user.save();
} catch (err) {
logger.error('/sendPrintRequest had an error: ', err);

Expand All @@ -145,4 +156,48 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => {
}
});

router.get('/status', async (req, res) => {
if (!checkIfTokenSent(req)) {
logger.warn('/status was requested without a token');
return res.sendStatus(UNAUTHORIZED);
}

const decodedToken = await decodeToken(req);
if (!decodedToken || Object.keys(decodedToken) === 0) {
logger.warn('/status was requested with an invalid token');
return res.sendStatus(UNAUTHORIZED);
}
if (!PRINTING.ENABLED) {
logger.warn('Printing is disabled, returning 200 and completed status to mock the printing server');
return res.status(OK).send({ status: 'completed' });
}

try {
const response = await fetch(PRINTER_URL + `/status/?id=${req.query.id}`, {
method: 'GET',
});

const user = await User.findById(decodedToken._id);

// { status: string }
const json = await response.json();
const pages = Number(req.query.pages);

if (json.status === 'completed') {
user.pagesPrinted += pages;
user.escrowPagesPrinted -= pages;
await user.save();
}

if (json.status === 'failed') {
user.escrowPagesPrinted -= pages;
await user.save();
}

res.status(OK).send(json);
} catch (err) {
res.sendStatus(SERVER_ERROR);
}
});

module.exports = router;
3 changes: 2 additions & 1 deletion api/main_endpoints/routes/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ router.post('/search', function(req, res) {
lastLogin: result.lastLogin,
membershipValidUntil: result.membershipValidUntil,
pagesPrinted: result.pagesPrinted,
escrowPagesPrinted: result.escrowPagesPrinted,
doorCode: result.doorCode,
_id: result._id
};
Expand Down Expand Up @@ -312,7 +313,7 @@ router.post('/getPagesPrintedCount', (req, res) => {
.status(NOT_FOUND)
.send({ message: `${req.body.email} not found.` });
}
return res.status(OK).json(result.pagesPrinted);
return res.status(OK).json(result.pagesPrinted + result.escrowPagesPrinted);
});
});

Expand Down
18 changes: 18 additions & 0 deletions src/APIFunctions/2DPrinting.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ export function parseRange(pages, maxPages) {
return result;
}

export async function getPrintStatus(printId, totalPages, token) {
const url = new URL('/api/Printer/status', BASE_API_URL);

const response = await fetch(url.href + `?id=${printId}&pages=${totalPages}`, {
headers: {
'Authorization': `Bearer ${token}`
},
method: 'GET',
});

const json = await response.json();
const status = json.status;

return status;
}

/**
* Print the page
* @param {Object} data - PDF File and its configurations
Expand All @@ -80,6 +96,7 @@ export async function printPage(data, token) {
const pdf = data.get('file');
const sides = data.get('sides');
const copies = data.get('copies');
const totalPages = data.get('totalPages');
const id = crypto.randomUUID();
const CHUNK_SIZE = 1024 * 1024 * 0.5; // 0.5 MB ------- SENT DATA **CANNOT** EXCEED 1 MB
const totalChunks = Math.ceil(pdf.size / CHUNK_SIZE);
Expand All @@ -98,6 +115,7 @@ export async function printPage(data, token) {
chunkData.append('id', id);
chunkData.append('sides', sides);
chunkData.append('copies', copies);
chunkData.append('totalPages', totalPages);
}

try {
Expand Down
2 changes: 2 additions & 0 deletions src/APIFunctions/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export async function editUser(userToEdit, token) {
discordDiscrim,
discordID,
pagesPrinted,
escrowPagesPrinted,
accessLevel,
lastLogin,
emailVerified,
Expand All @@ -122,6 +123,7 @@ export async function editUser(userToEdit, token) {
discordDiscrim,
discordID,
pagesPrinted,
escrowPagesPrinted,
accessLevel,
lastLogin,
emailVerified,
Expand Down
12 changes: 12 additions & 0 deletions src/Components/Printing/JobStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';

export default function JobStatus(props) {
return (
<div key={props.id} className='flex items-center justify-center w-full mt-10'>
<div role="alert" className={'w-1/2 text-center alert alert-' + (props.status === 'failed' ? 'error' : 'success')}>
<svg xmlns="http://www.w3.org/2000/svg" className="w-6 h-6 stroke-current shrink-0" fill="none" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 13V8m0 8h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>
<p className=''>{props.fileName} ({props.id}): {props.status}</p>
</div>
</div>
);
}
76 changes: 64 additions & 12 deletions src/Pages/2DPrinting/2DPrinting.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import {
parseRange,
printPage,
getPagesPrinted,
getPrintStatus,
} from '../../APIFunctions/2DPrinting';
import { editUser } from '../../APIFunctions/User';

import { PDFDocument } from 'pdf-lib';
import { healthCheck } from '../../APIFunctions/2DPrinting';
import ConfirmationModal from
'../../Components/DecisionModal/ConfirmationModal.js';

import { useSCE } from '../../Components/context/SceContext.js';
import JobStatus from '../../Components/Printing/JobStatus.js';

export default function Printing() {
const { user, setUser } = useSCE();
Expand All @@ -33,6 +34,7 @@ export default function Printing() {
const [printerHealthy, setPrinterHealthy] = useState(false);
const [loading, setLoading] = useState(true);
const [PdfFile, setPdfFile] = useState(null);
const [printJobs, setPrintJobs] = useState({});

async function checkPrinterHealth() {
setLoading(true);
Expand All @@ -53,6 +55,49 @@ export default function Printing() {
}

useEffect(() => {
if (printJobs === null || Object.keys(printJobs).length === 0) return;
const ids = Object.keys(printJobs);

const interval = setInterval(async () => {
if (ids.length === 0) {
clearInterval(interval);
return;
}

ids.map(async (id) => {
if (['completed', 'failed'].includes(printJobs[id].status)) return;

const status = await getPrintStatus(id, printJobs[id].pages, user.token);
const newPrintJobs = {...printJobs};

if (['completed', 'failed'].includes(status)) {
setTimeout(() => {
setPrintJobs((prev) => {
const newPrintJobs = {...prev};
delete newPrintJobs[id];
window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs));
return {...newPrintJobs};
});
}, 5000);
}

newPrintJobs[id].status = status;
setPrintJobs(newPrintJobs);
window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs));
});
}, 1000);

return () => clearInterval(interval);
}, [printJobs]);

useEffect(() => {
if (!!window.localStorage) {
const jobsFromLocal = JSON.parse(window.localStorage.getItem('printJobs'));
if (!!jobsFromLocal) {
setPrintJobs(jobsFromLocal);
}
}

checkPrinterHealth();
getNumberOfPagesPrintedSoFar();
}, []);
Expand Down Expand Up @@ -200,20 +245,20 @@ export default function Printing() {
data.append('file', PdfFile);
data.append('sides', sides);
data.append('copies', copies);
let status = await printPage(data, user.token);
data.append('totalPages', pagesToBeUsedInPrintRequest);
const printReq = await printPage(data, user.token);

if (!status.error) {
editUser(
{ ...user, pagesPrinted: pagesPrinted + pagesToBeUsedInPrintRequest },
user.token,
);
setPrintStatus('Printing succeeded!');
setPrintStatusColor('success');
} else {
try {
const printId = printReq?.responseData['print_id'];
const newPrintJobs = {...printJobs, [printId]: {status: 'created', fileName: PdfFile.name, pages: pagesToBeUsedInPrintRequest} };
setPrintJobs(newPrintJobs);
window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs));
getNumberOfPagesPrintedSoFar();
} catch (err) {
setPrintStatus('Printing failed. Please try again or reach out to SCE Dev team if the issue persists.');
setPrintStatusColor('error');
}
getNumberOfPagesPrintedSoFar();

setTimeout(() => {
setPrintStatus(null);
}, 5000);
Expand Down Expand Up @@ -417,6 +462,14 @@ export default function Printing() {

return (
<div className='w-full'>
<div>
{
Object.keys(printJobs).map(id => (
<JobStatus id={id} status={printJobs[id].status} fileName={printJobs[id].fileName} />
))
}
</div>

<ConfirmationModal {... {
headerText: 'Submit print request?',
bodyText: `The request will use ${pagesToBeUsedInPrintRequest} page(s) out of the ${getRemainingPageBalance()} pages remaining.`,
Expand Down Expand Up @@ -450,4 +503,3 @@ export default function Printing() {
</div>
);
}