From cb948a160bb03baa45586009d2bca83cc442ba18 Mon Sep 17 00:00:00 2001 From: rdbsolutions Date: Tue, 23 Sep 2025 12:44:21 +0100 Subject: [PATCH 1/3] updated admin tabs and filtering --- actions/index.js | 21 +- components/admin/tabs/accounts.js | 2 + components/admin/tabs/documents.js | 265 ++++++++++++++++++---- pages/admin/index.js | 71 +++--- pages/api/admin/getlatestdocuments_api.js | 26 ++- 5 files changed, 299 insertions(+), 86 deletions(-) diff --git a/actions/index.js b/actions/index.js index e995a893..fd04e4a8 100644 --- a/actions/index.js +++ b/actions/index.js @@ -2144,8 +2144,25 @@ export const getNewDocumentsPaged = async ( fieldSort, showNumberOfRecords, documentType, + documentOrigin, selectedWeeks ) => { + console.log( + "/api/admin/getlatestdocuments_api?pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + + "&documentType=" + + documentType + + "&numberWeeks=" + + selectedWeeks + + "&documentOrigin=" + + documentOrigin + ); return axios .get( "/api/admin/getlatestdocuments_api?pageNumber=" + @@ -2159,7 +2176,9 @@ export const getNewDocumentsPaged = async ( "&documentType=" + documentType + "&numberWeeks=" + - selectedWeeks + selectedWeeks + + "&documentOrigin=" + + documentOrigin ) .then((res) => { return res.data; diff --git a/components/admin/tabs/accounts.js b/components/admin/tabs/accounts.js index 97498248..8d802860 100644 --- a/components/admin/tabs/accounts.js +++ b/components/admin/tabs/accounts.js @@ -26,6 +26,8 @@ export default function AccountsTab({ userData }) { > {user.email} +
+ {user.id}
Last accessed: {user.created} diff --git a/components/admin/tabs/documents.js b/components/admin/tabs/documents.js index 23818da2..94bc359a 100644 --- a/components/admin/tabs/documents.js +++ b/components/admin/tabs/documents.js @@ -7,6 +7,9 @@ import { connect } from "react-redux"; import transLookup from "../../../data/lookuptranslations.json"; import { formatDates } from "../../utils"; import PaginationControl from "../../../components/case/pagination"; +import DocumentLink from "../../utils/downloads"; +import { useDownloadQueue } from "../../utils/downloadmanager"; + import { sendGAEvent } from "@next/third-parties/google"; import { @@ -170,6 +173,21 @@ const getDocumentTypes = [ }, ]; +const getOrigin = [ + { + "Value": 846040000, + "Label": "MDU", + }, + { + "Value": 846040001, + "Label": "Portal", + }, + { + "Value": 846040002, + "Label": "Manual", + }, +]; + const DocumentsTab = (props) => { let { t, lang } = useTranslation(); const firstRender = useRef(false); @@ -189,12 +207,15 @@ const DocumentsTab = (props) => { const { locale } = router; var documentDetailsArr = documentDetailsObj || []; - + const { startDownload, getStatus } = useDownloadQueue(3); const [selectedOption, setSelectedOption] = useState(10); const [selectedWeeks, setSelectedWeeks] = useState(1); const [documentTypes, setDocumentTypes] = useState(getDocumentTypes); + const [documentOrigin, setDocumentOrigin] = useState(getOrigin); const [selectedDocumentType, setSelectedDocumentType] = useState("all"); + const [selectedDocumentOrigin, setSelectedDocumentOrigin] = useState("all"); const [selectAll, setSelectAll] = useState(false); + const [selectOriginAll, setSelectOriginAll] = useState(false); const [checkedItems, setCheckedItems] = useState( documentTypes.reduce((acc, item) => { @@ -203,6 +224,13 @@ const DocumentsTab = (props) => { }, {}) ); + const [checkedOriginItems, setCheckedOriginItems] = useState( + documentOrigin.reduce((acc, item) => { + acc[item.pinswg_origin] = false; + return acc; + }, {}) + ); + const handleCheckboxChange = (e) => { const { name, checked } = e.target; @@ -213,6 +241,16 @@ const DocumentsTab = (props) => { setSelectAll(false); }; + const handleCheckboxOriginChange = (e) => { + const { name, checked } = e.target; + + setCheckedOriginItems((prev) => ({ + ...prev, + [name]: checked, + })); + setSelectOriginAll(false); + }; + const handleSelectAllChange = (e) => { const { checked } = e.target; setSelectAll(checked); // Set the 'Select All' state @@ -230,16 +268,42 @@ const DocumentsTab = (props) => { } }; + const handleSelectOriginAllChange = (e) => { + const { checked } = e.target; + setSelectOriginAll(checked); // Set the 'Select All' state + if (checked) { + // If 'Select All' is checked, check all the other checkboxes + + setCheckedOriginItems( + documentOrigin.reduce((acc, item) => { + acc[item.pinswg_origin] = true; + return acc; + }, {}) + ); + } else { + clearCheckboxes(); + } + }; + const clearCheckboxes = () => { setSelectAll(false); + setSelectOriginAll(false); setCheckedItems( documentTypes.reduce((acc, item) => { acc[item.pinswg_isharedocumentlocations] = false; return acc; }, {}), + setSelectedDocumentType("all"), setCurrentPage(1) ); + setCheckedOriginItems( + documentOrigin.reduce((acc, item) => { + acc["origin_" + item.pinswg_origin] = false; + return acc; + }, {}), + setSelectedDocumentOrigin("all") + ); }; let ShowDocLinks = false; let checkShowDocLinks = docsOffline != false ? true : false; @@ -314,6 +378,9 @@ const DocumentsTab = (props) => { {t("search:clear-filter-button-label")} +

+ Document Type +

{" "} <> @@ -335,17 +402,6 @@ const DocumentsTab = (props) => { {router.locale == "cy" ? "Pawb" : "All"}{" "} - - ( - {documentTypes.reduce( - (accumulator, currentItem) => { - return ( - accumulator + - currentItem.count - ); - }, - 0 - )} - )
@@ -389,17 +445,105 @@ const DocumentsTab = (props) => { ); })} {" "} +

+ Document Origin +

+
+ {" "} + <> +
+ + +
+ + {documentOrigin.map((item, key) => { + return ( +
+ + +
+ ); + })} +
{" "}
diff --git a/pages/api/admin/getlatestdocuments_api.js b/pages/api/admin/getlatestdocuments_api.js index c5742dab..36b7117c 100644 --- a/pages/api/admin/getlatestdocuments_api.js +++ b/pages/api/admin/getlatestdocuments_api.js @@ -64,9 +64,11 @@ export default async function ApiProxy(req, res) { var fieldSort = req.query.fieldSort || "desc"; var showNumberOfRecords = req.query.showNumberOfRecords || 10; var documentType = req.query.documentType || "all"; + var documentOrigin = req.query.documentOrigin || "all"; var numberOfWeeks = req.query.numberWeeks || 1; var docTypeQueryString = ""; + var docOriginQueryString = ""; if (documentType != "all") { if (documentType.indexOf(",") >= 0) { @@ -91,6 +93,27 @@ export default async function ApiProxy(req, res) { } } + if (documentOrigin != "all") { + if (documentOrigin.indexOf(",") >= 0) { + const documentOriginArr = documentOrigin.split(","); + + docOriginQueryString = " and ("; + documentOriginArr.forEach(function (item, index) { + console.log(item, index); + + if (item != "all") { + docOriginQueryString += " pinswg_origin eq " + item; + + docOriginQueryString += + index < documentOriginArr.length - 1 ? " or " : ""; + } + }); + docOriginQueryString += " )"; + } else { + docOriginQueryString += " and pinswg_origin eq " + documentOrigin; + } + } + function daysAgoISO(days) { const today = new Date(); const resultDate = new Date(today); @@ -103,6 +126,7 @@ export default async function ApiProxy(req, res) { "pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " + daysAgoISO(numberOfWeeks) + docTypeQueryString + + docOriginQueryString + "&$orderby=" + orderby + " " + @@ -115,7 +139,7 @@ export default async function ApiProxy(req, res) { console.log( "\n==========================================\n", - "\nnew docs search ", + "\nnew docs search---- ", "\n\nQuery url: " + queryUrl, "\n\nRelay link: " + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), "\n==========================================\n" From 0b3cb9778b30d41c40b248df62238129a783a3a2 Mon Sep 17 00:00:00 2001 From: rdbsolutions Date: Tue, 23 Sep 2025 12:45:20 +0100 Subject: [PATCH 2/3] queued downloads and stream not buffer of downloads --- .gitignore | 2 + components/case/documents.js | 96 +++++--- components/utils/downloadmanager.js | 48 ++++ components/utils/downloads.js | 324 +++++++++++++++++++++++++ pages/api/documents/download/[id].js | 295 ++++++++++++++-------- styles/sass/welshgov/_application.scss | 20 ++ 6 files changed, 648 insertions(+), 137 deletions(-) create mode 100644 components/utils/downloadmanager.js create mode 100644 components/utils/downloads.js diff --git a/.gitignore b/.gitignore index 7485fbf5..fe4298be 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ public/swagger.json certificates/cert-key.pem certificates/cert.pem gitclean.sh +pages/admin/logcheck.js +pages/admin/logchecker.js diff --git a/components/case/documents.js b/components/case/documents.js index 15af88c4..35f6db4a 100644 --- a/components/case/documents.js +++ b/components/case/documents.js @@ -17,6 +17,8 @@ import { import { setCurrentPage } from "../../store/currentView/action"; import { setDocumentDetails } from "../../store/searchOutput/action"; +import DocumentLink from "../utils/downloads"; +import { useDownloadQueue } from "../utils/downloadmanager"; const DocumentDetails = (props) => { let { t, lang } = useTranslation(); @@ -37,7 +39,7 @@ const DocumentDetails = (props) => { const { locale } = router; var documentDetailsArr = documentDetailsObj || []; - + const { startDownload, getStatus } = useDownloadQueue(3); const [selectedOption, setSelectedOption] = useState(10); const [documentTypes, setDocumentTypes] = useState([{}]); const [selectedDocumentType, setSelectedDocumentType] = useState("all"); @@ -764,41 +766,63 @@ const DocumentDetails = (props) => { )} {!ShowDocLinks ? ( - { - toggleDownLoadLink( - key - ), - sendGAEvent( - "event", - "DownloadedFile", - { - caseReference: - props.caseReference, - filename: - detailsObj.pinswg_name, - } - ); - }} - > - - {t( - "case:documents-name-label" - )} - : - - {_.has( - detailsObj, - "pinswg_name" - ) - ? detailsObj.pinswg_name - : ""} - + <> + + {/* { + toggleDownLoadLink( + key + ), + sendGAEvent( + "event", + "DownloadedFile", + { + caseReference: + props.caseReference, + filename: + detailsObj.pinswg_name, + } + ); + }} + > + + {t( + "case:documents-name-label" + )} + : + + {_.has( + detailsObj, + "pinswg_name" + ) + ? detailsObj.pinswg_name + : ""} + */} + ) : ( <> diff --git a/components/utils/downloadmanager.js b/components/utils/downloadmanager.js new file mode 100644 index 00000000..eb16d0bd --- /dev/null +++ b/components/utils/downloadmanager.js @@ -0,0 +1,48 @@ +import { useState, useCallback } from "react"; + +export function useDownloadQueue(maxConcurrent = 3) { + const [queue, setQueue] = useState([]); // queued tasks + const [activeCount, setActiveCount] = useState(0); + const [statuses, setStatuses] = useState({}); // { id: "idle"|"queued"|"downloading"|"done"|"failed" } + + const scheduleNext = useCallback(() => { + setQueue((queueCurr) => { + if (activeCount >= maxConcurrent || queueCurr.length === 0) + return queueCurr; + + const [task, ...rest] = queueCurr; + + setStatuses((s) => ({ ...s, [task.id]: "downloading" })); + setActiveCount((c) => c + 1); + + // Run the download fully in async IIFE + (async () => { + try { + await task.downloadFn(); // triggers fetch + blob + click + setStatuses((s) => ({ ...s, [task.id]: "done" })); + } catch (err) { + console.error(err); + setStatuses((s) => ({ ...s, [task.id]: "failed" })); + } finally { + setActiveCount((c) => c - 1); + scheduleNext(); // promote next in queue + } + })(); + + return rest; + }); + }, [activeCount, maxConcurrent]); + + const startDownload = useCallback( + (id, downloadFn) => { + setStatuses((s) => ({ ...s, [id]: "queued" })); + setQueue((curr) => [...curr, { id, downloadFn }]); + scheduleNext(); + }, + [scheduleNext] + ); + + const getStatus = useCallback((id) => statuses[id] || "idle", [statuses]); + + return { startDownload, getStatus }; +} diff --git a/components/utils/downloads.js b/components/utils/downloads.js new file mode 100644 index 00000000..7283c030 --- /dev/null +++ b/components/utils/downloads.js @@ -0,0 +1,324 @@ +// // import { useState } from "react"; + +// // export default function DocumentLink({ detailsObj, caseReference }) { +// // const [downloadStatus, setDownloadStatus] = useState(""); +// // const [progress, setProgress] = useState(null); + +// // const handleDownload = async () => { +// // setDownloadStatus("Downloading"); +// // setProgress(0); + +// // try { +// // const res = await fetch(detailsObj.pinswg_hashlink); +// // if (!res.ok) throw new Error("Download failed"); + +// // const contentDisposition = res.headers.get("content-disposition"); +// // const filename = contentDisposition +// // ? contentDisposition.split("filename=")[1] +// // : detailsObj.pinswg_name || "document"; + +// // const contentLength = res.headers.get("content-length"); +// // const totalBytes = contentLength +// // ? parseInt(contentLength, 10) +// // : null; + +// // const reader = res.body.getReader(); +// // let receivedBytes = 0; +// // const chunks = []; + +// // while (true) { +// // const { done, value } = await reader.read(); +// // if (done) break; + +// // chunks.push(value); +// // receivedBytes += value.length; + +// // if (totalBytes) { +// // const percent = Math.round( +// // (receivedBytes / totalBytes) * 100 +// // ); +// // setProgress(percent); +// // } +// // } + +// // // Combine chunks into one blob +// // const blob = new Blob(chunks); +// // const url = window.URL.createObjectURL(blob); + +// // const link = document.createElement("a"); +// // link.href = url; +// // link.download = filename; +// // document.body.appendChild(link); +// // link.click(); +// // document.body.removeChild(link); + +// // setDownloadStatus("Download complete!"); +// // setProgress(100); + +// // // Fire GA event +// // window.gtag?.("event", "DownloadedFile", { +// // caseReference, +// // filename, +// // }); +// // } catch (err) { +// // console.error(err); +// // setDownloadStatus("Download failed"); +// // setProgress(null); +// // } +// // }; + +// // return ( +// // <> +// // {detailsObj.pinswg_publishtoweb ? ( +// // +// // ) : ( +// // <> +// // +// // Document name: +// // +// // {detailsObj.pinswg_name || ""} +// // +// // )} + +// // {downloadStatus && ( +// //

+// // {downloadStatus} +// // {/* {progress !== null && ` (${progress}%)`} */} +// //

+// // )} +// // +// // ); +// // } +// import { useState } from "react"; + +// export default function DocumentLink({ +// id, +// detailsObj, +// caseReference, +// startDownload, +// getStatus, +// }) { +// const [downloadStatus, setDownloadStatus] = useState(""); +// const [progress, setProgress] = useState(null); + +// const downloadFile = async () => { +// setDownloadStatus("Downloading"); +// setProgress(0); + +// try { +// const res = await fetch(detailsObj.pinswg_hashlink); +// if (!res.ok) throw new Error("Download failed"); + +// const contentDisposition = res.headers.get("content-disposition"); +// const filename = contentDisposition +// ? contentDisposition.split("filename=")[1] +// : detailsObj.pinswg_name || "document"; + +// const contentLength = res.headers.get("content-length"); +// const totalBytes = contentLength +// ? parseInt(contentLength, 10) +// : null; + +// const reader = res.body.getReader(); +// let receivedBytes = 0; +// const chunks = []; + +// while (true) { +// const { done, value } = await reader.read(); +// if (done) break; + +// chunks.push(value); +// receivedBytes += value.length; + +// if (totalBytes) { +// const percent = Math.round( +// (receivedBytes / totalBytes) * 100 +// ); +// setProgress(percent); +// } +// } + +// const blob = new Blob(chunks); +// const url = window.URL.createObjectURL(blob); + +// const link = document.createElement("a"); +// link.href = url; +// link.download = filename; +// document.body.appendChild(link); +// link.click(); +// document.body.removeChild(link); + +// setDownloadStatus("Download complete!"); +// setProgress(100); + +// window.gtag?.("event", "DownloadedFile", { +// caseReference, +// filename, +// }); +// } catch (err) { +// console.error(err); +// setDownloadStatus("Download failed"); +// setProgress(null); +// } +// }; + +// const handleClick = () => { +// if (getStatus(id) !== "idle") return; // don't double enqueue +// startDownload(id, downloadFile); +// }; + +// const status = getStatus(id); + +// return ( +// <> +// {detailsObj.pinswg_publishtoweb ? ( +// +// ) : ( +// <> +// +// Document name: +// +// {detailsObj.pinswg_name || ""} +// +// )} + +// {status !== "idle" && ( +//

+// {status === "downloading" && progress !== null +// ? `Downloading (${progress}%)` +// : status === "queued" +// ? "Queued" +// : status === "done" +// ? "Download complete!" +// : status === "failed" +// ? "Download failed" +// : ""} +//

+// )} +// +// ); +// } + +import { useState } from "react"; + +export default function DocumentLink({ + id, + detailsObj, + caseReference, + startDownload, + getStatus, +}) { + const [progress, setProgress] = useState(null); + + const downloadFile = async () => { + setProgress(0); + + const res = await fetch(detailsObj.pinswg_hashlink); + if (!res.ok) throw new Error("Download failed"); + + const contentDisposition = res.headers.get("content-disposition"); + const filename = contentDisposition + ? contentDisposition.split("filename=")[1] + : detailsObj.pinswg_name || "document"; + + const contentLength = res.headers.get("content-length"); + const totalBytes = contentLength ? parseInt(contentLength, 10) : null; + + const reader = res.body.getReader(); + let receivedBytes = 0; + const chunks = []; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + chunks.push(value); + receivedBytes += value.length; + + if (totalBytes) { + const percent = Math.round((receivedBytes / totalBytes) * 100); + setProgress(percent); + } + } + + const blob = new Blob(chunks); + const url = window.URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + setProgress(100); + window.gtag?.("event", "DownloadedFile", { caseReference, filename }); + }; + + const handleClick = () => { + if (getStatus(id) !== "idle") return; // avoid double enqueue + startDownload(id, downloadFile); + }; + + const status = getStatus(id); + + return ( +
+ {detailsObj.pinswg_publishtoweb ? ( + + ) : ( + + {detailsObj.pinswg_name || ""} + + )} + + {status !== "idle" && ( +

+ {status === "downloading" && progress !== null + ? `Downloading ` + : status === "queued" + ? "Queued" + : status === "done" + ? "Download complete!" + : status === "failed" + ? "Download failed" + : ""} +

+ )} +
+ ); +} diff --git a/pages/api/documents/download/[id].js b/pages/api/documents/download/[id].js index 044e62b9..cbc8db9c 100644 --- a/pages/api/documents/download/[id].js +++ b/pages/api/documents/download/[id].js @@ -1,110 +1,203 @@ -/** - * @swagger - * /api/documents/download/{id}: - * get: - * tags: - * - Documents - * description: Get document - * parameters: - * - name: id - * in: path - * description: iShare ID - * type: string - * required: true - * default: A41567436 - * - name: hash - * in: query - * description: Hash string - * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71 - * responses: - * 200: - * description: Success - */ +// /** +// * @swagger +// * /api/documents/download/{id}: +// * get: +// * tags: +// * - Documents +// * description: Get document +// * parameters: +// * - name: id +// * in: path +// * description: iShare ID +// * type: string +// * required: true +// * default: A41567436 +// * - name: hash +// * in: query +// * description: Hash string +// * default: e0a1c9cd2817826a25bca67e60b807eae747cbed769e3ec42cf997541c32be71 +// * responses: +// * 200: +// * description: Success +// */ + +// import axios from "axios"; +// import CryptoJS from "crypto-js"; +// import { getToken, consoleLogger } from "../../../../actions"; + +// const WORDKEY = process.env.HASHKEY; +// const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT; +// const tenantId = process.env.TENANT; + +// const WEBAPI_URL = +// process.env.RELAY_ROOT || +// "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; + +// const hashAPIPath = (queryPath) => { +// var hashlink = CryptoJS.HmacSHA256( +// queryPath, +// CryptoJS.enc.Hex.parse(WORDKEY) +// ); +// hashlink = hashlink.toString(CryptoJS.enc.Hex); + +// //return "&hash=" + hashlink; + +// return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; +// }; + +// const azureHeadersPaged = (access_token) => { +// return { +// headers: { +// "OData-MaxVersion": "4.0", +// "OData-Version": "4.0", +// "Accept": "application/json;odata.metadata=none", +// "Prefer": +// 'odata.include-annotations="*",return=representation, odata.maxpagesize=10', +// "Content-Type": "application/json", +// "Authorization": "Bearer " + access_token, +// }, +// }; +// }; + +// export default async function ApiProxy(req, res) { +// var token = await getToken(); + +// var docRef = req.query; + +// var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash; + +// const configDocument = (access_token) => { +// return { +// headers: { +// "OData-MaxVersion": "4.0", +// "OData-Version": "4.0", +// "Accept": "application/json", +// "Prefer": 'odata.include-annotations="*",return=representation', +// "Content-Type": "application/json", +// "Authorization": "Bearer " + access_token, +// }, +// responseType: "arraybuffer", +// }; +// }; + +// return axios +// .get( +// WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl), +// configDocument(token.access_token) +// ) +// .then((response) => { +// const bytes = response.data.byteLength; +// console.log(bytes); + +// res.setHeader( +// "content-disposition", +// "attachment; filename=" + +// response.headers["content-disposition"].split( +// "filename=" +// )[1] +// ); +// return res.status(200).send(response.data); +// }) +// .then(() => { +// console.log(docRef.id + "has downloaded"); + +// return "downloadComplete"; +// }) +// .catch((error) => { +// consoleLogger(error); +// res.redirect("/filenotavailable"); +// }); +// } import axios from "axios"; -import CryptoJS from "crypto-js"; import { getToken, consoleLogger } from "../../../../actions"; -const WORDKEY = process.env.HASHKEY; -const accessTokenEndpoint = process.env.ACCESS_TOKEN_ENDPOINT; -const tenantId = process.env.TENANT; - const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; -const hashAPIPath = (queryPath) => { - var hashlink = CryptoJS.HmacSHA256( - queryPath, - CryptoJS.enc.Hex.parse(WORDKEY) - ); - hashlink = hashlink.toString(CryptoJS.enc.Hex); - - //return "&hash=" + hashlink; - - return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; -}; - -const azureHeadersPaged = (access_token) => { - return { - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - "Accept": "application/json;odata.metadata=none", - "Prefer": - 'odata.include-annotations="*",return=representation, odata.maxpagesize=10', - "Content-Type": "application/json", - "Authorization": "Bearer " + access_token, - }, - }; -}; - -export default async function ApiProxy(req, res) { - var token = await getToken(); - - var docRef = req.query; - - var queryUrl = "documents/download/" + docRef.id + "?hash=" + docRef.hash; - - const configDocument = (access_token) => { - return { - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - "Accept": "application/json", - "Prefer": 'odata.include-annotations="*",return=representation', - "Content-Type": "application/json", - "Authorization": "Bearer " + access_token, - }, - responseType: "arraybuffer", - }; - }; - - return axios - .get( - WEBAPI_URL + queryUrl, // + hashAPIPath(queryUrl), - configDocument(token.access_token) - ) - .then((response) => { - const bytes = response.data.byteLength; - console.log(bytes); - - res.setHeader( - "content-disposition", - "attachment; filename=" + - response.headers["content-disposition"].split( - "filename=" - )[1] - ); - return res.status(200).send(response.data); - }) - .then(() => { - console.log(docRef.id + "has downloaded"); - - return "downloadComplete"; - }) - .catch((error) => { - consoleLogger(error); - res.redirect("/filenotavailable"); - }); +// Retry utility with logging +async function retry(fn, retries = 3, delay = 1000) { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return { response: await fn(), attempts: attempt }; + } catch (err) { + if (attempt === retries) throw err; + console.log(`Retry ${attempt} failed, retrying in ${delay}ms...`); + await new Promise((res) => setTimeout(res, delay)); + delay *= 2; // exponential backoff + } + } } + +const ApiProxy = async (req, res) => { + const docRef = req.query; + + try { + const token = await getToken(); + const startTime = Date.now(); + + const fetchStream = async () => { + const queryUrl = + "documents/download/" + docRef.id + "?hash=" + docRef.hash; + + return axios.get(WEBAPI_URL + queryUrl, { + headers: { + "OData-MaxVersion": "4.0", + "OData-Version": "4.0", + "Accept": "application/json", + "Prefer": + 'odata.include-annotations="*",return=representation', + "Content-Type": "application/json", + "Authorization": "Bearer " + token.access_token, + }, + responseType: "stream", + }); + }; + + const { response, attempts } = await retry(fetchStream, 3, 1000); + + // Extract filename + const contentDisposition = response.headers["content-disposition"]; + const filename = contentDisposition + ? contentDisposition.split("filename=")[1] + : docRef.id; + + res.setHeader( + "Content-Disposition", + `attachment; filename=${filename}` + ); + res.setHeader("Content-Type", "application/octet-stream"); + + let totalBytes = 0; + + response.data.on("data", (chunk) => { + totalBytes += chunk.length; + }); + + response.data.pipe(res); + + response.data.on("end", () => { + const durationMs = Date.now() - startTime; + console.log( + `[Download Complete] Document: ${filename}, ID: ${docRef.id}, Size: ${totalBytes} bytes, Duration: ${durationMs}ms, Attempts: ${attempts}` + ); + }); + + response.data.on("error", (err) => { + consoleLogger(err); + if (!res.headersSent) res.redirect("/filenotavailable"); + }); + } catch (error) { + consoleLogger(error); + if (!res.headersSent) res.redirect("/filenotavailable"); + } +}; + +export const config = { + api: { + responseLimit: false, + }, +}; + +export default ApiProxy; diff --git a/styles/sass/welshgov/_application.scss b/styles/sass/welshgov/_application.scss index b80401b7..9e2f3b14 100644 --- a/styles/sass/welshgov/_application.scss +++ b/styles/sass/welshgov/_application.scss @@ -1469,6 +1469,22 @@ textarea, top: calc(50% - 10px); } } + + .documentLink { + border: none; + text-align: left; + cursor: pointer; + color: #0360a6; + display: block; + padding-left: 0px; + + &:hover { + background-color: $white; + color: #3b7dc5; + border: none; + padding-left: 0px + } + } } .appealModule { @@ -2932,6 +2948,10 @@ fade { .govuk-label-s { font-size: 14px; } + + &.govuk-checkboxes_short { + height: 180px; + } } From 118d8a56c7adc5677eef9cf5ec6037fe413870f1 Mon Sep 17 00:00:00 2001 From: rdbsolutions Date: Wed, 1 Oct 2025 09:18:31 +0100 Subject: [PATCH 3/3] welsh language labels --- .gitignore | 1 + components/utils/downloads.js | 11 ++++++----- locales/cy/case.json | 6 +++++- locales/en/case.json | 6 +++++- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index fe4298be..ad55dc2c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ certificates/cert.pem gitclean.sh pages/admin/logcheck.js pages/admin/logchecker.js +pages/baracuda.html diff --git a/components/utils/downloads.js b/components/utils/downloads.js index 7283c030..6e8c81b4 100644 --- a/components/utils/downloads.js +++ b/components/utils/downloads.js @@ -222,6 +222,7 @@ // } import { useState } from "react"; +import useTranslation from "next-translate/useTranslation"; export default function DocumentLink({ id, @@ -231,7 +232,7 @@ export default function DocumentLink({ getStatus, }) { const [progress, setProgress] = useState(null); - + let { t } = useTranslation(); const downloadFile = async () => { setProgress(0); @@ -309,13 +310,13 @@ export default function DocumentLink({ } > {status === "downloading" && progress !== null - ? `Downloading ` + ? t("case:downloading-label") : status === "queued" - ? "Queued" + ? t("case:download-queued-label") : status === "done" - ? "Download complete!" + ? t("case:download-complete-label") : status === "failed" - ? "Download failed" + ? t("case:download-failed-label") : ""}

)} diff --git a/locales/cy/case.json b/locales/cy/case.json index a0490f05..da629c24 100644 --- a/locales/cy/case.json +++ b/locales/cy/case.json @@ -100,5 +100,9 @@ "do-you-want-to-delete-case-disclaimer-label": "Ar ôl dileu, bydd yr holl wybodaeth a gyflwynwyd gennych yn cael ei cholli.", "representation-date-passed-label": "Mae'r cyfnod cyflwyno sylwadau ar gyfer yr apêl hon rhwng {{startDate}} a {{endDate}}", "representation-date-passed-additional-label": "Gallwch wneud sylw drwy e-bostio,", - "date-raised": "Dyddiad codi" + "date-raised": "Dyddiad codi", + "download-complete-label": "Lawrlwytho wedi'i gwblhau!", + "downloading-label": "Wrthi'n llwytho i lawr", + "download-failed-label": "Methodd y lawrlwythiad", + "download-queued-label": "Wedi'i giwio" } \ No newline at end of file diff --git a/locales/en/case.json b/locales/en/case.json index fac349c1..25e02d0c 100644 --- a/locales/en/case.json +++ b/locales/en/case.json @@ -100,5 +100,9 @@ "do-you-want-to-delete-case-disclaimer-label": "Once deleted all information you have submited, will be lost.", "representation-date-passed-label": "The representation period for this appeal is between {{startDate}} and {{endDate}}", "representation-date-passed-additional-label": "You can make a representation by emailing,", - "date-raised": "Date raised" + "date-raised": "Date raised", + "download-complete-label": "Download complete!", + "downloading-label": "Downloading", + "download-failed-label": "Download failed", + "download-queued-label": "Queued" } \ No newline at end of file