Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
eb0d402
save
thebeninator Aug 29, 2025
b460d20
poll job status on print page; local storage first pass
thebeninator Aug 29, 2025
1c28d7b
delete printId from local storage on complete
thebeninator Aug 29, 2025
cffad4d
resume polling if printId still exists
thebeninator Aug 30, 2025
722627a
add undefined check for localStorage in useEffect
thebeninator Aug 30, 2025
dec483a
save
thebeninator Sep 1, 2025
4913ae0
save
thebeninator Sep 1, 2025
9db2980
use promise for tryResolvePrint
thebeninator Sep 2, 2025
0a57c26
convert /status to GET, require token
thebeninator Sep 8, 2025
86e70a9
multiple print job tracking first pass
thebeninator Sep 8, 2025
3f439a4
fix lint
thebeninator Sep 8, 2025
ff60a16
fix interval using stale jobs state
thebeninator Sep 8, 2025
b43fd3e
implement doodoo ui for active prints
thebeninator Sep 9, 2025
c17f4d6
store job in local immediately; remove guard clause for 'PRINTED' state
thebeninator Sep 10, 2025
f48d82f
some clean up
thebeninator Sep 10, 2025
b0f5497
FIX LINT
thebeninator Sep 10, 2025
2780a0b
check if localstorage printJobs exists first before setting state
thebeninator Sep 15, 2025
4c752de
undoodoo the doodoo ui first pass
thebeninator Sep 29, 2025
2ea0f20
implement page escrow
thebeninator Nov 15, 2025
186b49b
some cleanup; actually save escrow modify on print fail
thebeninator Nov 15, 2025
cc0b02e
failed/completed jobs notifs now persist for 5 secs
thebeninator Nov 18, 2025
eeb9887
fix fail/complete notif not being destroyed after page refresh
thebeninator Nov 24, 2025
772c12d
rename function
thebeninator Nov 24, 2025
08d2972
checkiftokensent
evanugarte Dec 22, 2025
5ec04a6
const user = await User.findById(decoded._id);
evanugarte Dec 22, 2025
9320b4f
get ready for some noise
evanugarte Dec 22, 2025
f9b4d55
const user = await User.findById(decoded.token._id);
evanugarte Dec 22, 2025
f74fe1d
debug zone
evanugarte Dec 22, 2025
a234803
logger.error('/status had an error: ', err);
evanugarte Dec 22, 2025
82aa228
const response = await axios.get(url);
evanugarte Dec 22, 2025
12c5b4f
const response = await axios.get(, {
evanugarte Dec 22, 2025
dde31b7
const user = await User.findById(decodedToken.token._id);
evanugarte Dec 22, 2025
5fb2128
one thing
evanugarte Dec 22, 2025
f821692
one
evanugarte Dec 22, 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
57 changes: 55 additions & 2 deletions api/main_endpoints/routes/Printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,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 @@ -81,6 +83,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => {
logger.warn('Printing is disabled, returning 200 and dummy print id to mock the printing server');
return res.status(OK).send({ printId: null });
}
const user = await User.findById(decoded.token._id);

const dir = path.join(__dirname, 'printing');
const { totalChunks, chunkIdx } = req.body;
Expand All @@ -90,7 +93,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 @@ -109,12 +112,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 @@ -130,6 +138,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 @@ -138,4 +149,46 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => {
}
});

router.get('/status', async (req, res) => {
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 axios.get(`${PRINTER_URL}/status/`, {
params: {
id: req.query.id,
}
});

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

// { status: string }
const json = response.data;
const pages = Math.abs(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) {
logger.error('/status had an error: ', 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 @@ -108,6 +108,7 @@ router.post('/search', async 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 @@ -310,7 +311,7 @@ router.post('/getPagesPrintedCount', async (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
20 changes: 20 additions & 0 deletions src/APIFunctions/2DPrinting.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ export function parseRange(pages, maxPages) {
return result;
}

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

const response = await fetch(url, {
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 +98,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 +117,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
14 changes: 14 additions & 0 deletions src/Components/Printing/JobStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';

export default function JobStatus(props) {
return (
<div 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>
);
}
100 changes: 88 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 @@ -52,7 +54,68 @@ export default function Printing() {
}
}

async function tryRemoveJob(status, id) {
const completedOrFailed = ['completed', 'failed'].includes(status);
if (!completedOrFailed) return;

setTimeout(() => {
setPrintJobs((prev) => {
const newPrintJobs = {...prev};

if (!(id in newPrintJobs)) {
return prev;
}

delete newPrintJobs[id];
window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs));
return {...newPrintJobs};
});
}, 5000);
}

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) => {
const completedOrFailed = ['completed', 'failed'].includes(printJobs[id].status);
if (completedOrFailed) return;

const status = await getPrintStatus(id, printJobs[id].pages, user.token);
const newPrintJobs = {...printJobs};
newPrintJobs[id].status = status;
setPrintJobs(newPrintJobs);
window.localStorage.setItem('printJobs', JSON.stringify(newPrintJobs));

tryRemoveJob(status, id);
});
}, 1000);

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

useEffect(() => {
if (!!window.localStorage) {
const jobsFromLocal = JSON.parse(window.localStorage.getItem('printJobs'));
if (!!jobsFromLocal) {
setPrintJobs(() => {
const ids = Object.keys(jobsFromLocal);
ids.map(async (id) => {
tryRemoveJob(jobsFromLocal[id].status, id);
});

return jobsFromLocal;
});
}
}

checkPrinterHealth();
getNumberOfPagesPrintedSoFar();
}, []);
Expand Down Expand Up @@ -200,20 +263,26 @@ 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 +486,14 @@ export default function Printing() {

return (
<div className='w-full'>
<div>
{
Object.keys(printJobs).map(id => (
<JobStatus key={id} 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 +527,3 @@ export default function Printing() {
</div>
);
}