From 7a59fd0cd4783f4787e3ae7ecf8009f35965ef87 Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 24 Jul 2026 14:33:41 -0400 Subject: [PATCH] Gave claude my back end and it came up with this front end --- package-lock.json | 8 +- package.json | 2 +- src/public/index.html | 446 ++++++++++++++++++++++++++++++++++++++++++ src/server.js | 222 +++++++++++++++++++++ src/test.js | 152 -------------- 5 files changed, 673 insertions(+), 157 deletions(-) create mode 100644 src/public/index.html create mode 100644 src/server.js delete mode 100644 src/test.js diff --git a/package-lock.json b/package-lock.json index 0bcfd87..f23f9b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "dependencies": { "axios": "^1.13.2", - "dotenv": "^17.2.3" + "dotenv": "^17.4.2" } }, "node_modules/asynckit": { @@ -61,9 +61,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 15f3cd4..3d2a034 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "dependencies": { "axios": "^1.13.2", - "dotenv": "^17.2.3" + "dotenv": "^17.4.2" } } diff --git a/src/public/index.html b/src/public/index.html new file mode 100644 index 0000000..f8af0ee --- /dev/null +++ b/src/public/index.html @@ -0,0 +1,446 @@ + + + + + +Closed ticket board + + + + + + +
+ +
+

Closed ticket board

+
+ Not connected +
+
+ +
+ +
+ +
+
+ + + + diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..b830c77 --- /dev/null +++ b/src/server.js @@ -0,0 +1,222 @@ + +require('dotenv').config({ quiet: true }); + +const path = require('path'); +const express = require('express'); +const axios = require('axios'); + +const { CLIENT_ID, CLIENT_SECRET, TECHNICIAN_NAMES, STATUS_NAMES } = process.env; +const PORT = process.env.PORT || 3000; + +if (!CLIENT_ID || !CLIENT_SECRET) { + console.error('Client id and secret missing from .env'); + process.exit(1); +} + +const technicians = (TECHNICIAN_NAMES ? TECHNICIAN_NAMES.split(',') : []).map(t => t.trim()).filter(Boolean); +const statuses = (STATUS_NAMES ? STATUS_NAMES.split(',') : []).map(s => s.trim()).filter(Boolean); + +const WINDOW_MINUTES = 10; +const CHECK_MINUTES = 1; + +let accessToken = null; + +const state = { + auth: 'disconnected', + verificationUrl: null, + userCode: null, + lastCheck: null, + lastError: null +}; + +const tickets = new Map(); + +async function getDeviceCode() { + const params = new URLSearchParams({ + client_id: CLIENT_ID, + scope: 'SDPOnDemand.requests.READ', + grant_type: 'device_request', + access_type: 'offline' + }); + + const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/code', params); + return res.data; +} + +function pollForToken(deviceCode, interval, expiresIn) { + const pollParams = new URLSearchParams({ + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + code: deviceCode, + grant_type: 'device_token' + }); + + return new Promise((resolve, reject) => { + const deadline = Date.now() + (expiresIn || 300) * 1000; + + const timer = setInterval(async () => { + if (Date.now() > deadline) { + clearInterval(timer); + reject(new Error('The authorization code expired. Connect again.')); + return; + } + + try { + const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/token', pollParams); + if (res.data.access_token) { + clearInterval(timer); + accessToken = res.data.access_token; + console.log('Authorization successful'); + resolve(); + } + } catch (err) { + } + }, interval + 500); + }); +} + +function toMillis(completedTime) { + if (!completedTime) return null; + const raw = typeof completedTime === 'object' ? completedTime.value : completedTime; + const ms = Number(raw); + return Number.isFinite(ms) ? ms : null; +} + +async function fetchClosedTickets() { + if (!accessToken) return; + + try { + const windowStart = Date.now() - WINDOW_MINUTES * 60 * 1000; + + const listInfo = { + list_info: { + row_count: 50, + sort_field: 'completed_time', + sort_order: 'desc', + fields_required: ['display_id', 'subject', 'status', 'completed_time', 'technician'], + search_criteria: [ + { + field: 'status.internal_name', + condition: 'is', + values: statuses.length ? statuses : ['Closed'] + }, + { + field: 'completed_time', + condition: 'greater_than', + value: windowStart.toString(), + logical_operator: 'and' + } + ] + } + }; + + const response = await axios.get('https://ithelp.mainecc.edu/api/v3/requests', { + params: { input_data: JSON.stringify(listInfo) }, + headers: { + Authorization: `Zoho-oauthtoken ${accessToken}`, + Accept: 'application/vnd.manageengine.sdp.v3+json' + } + }); + + const found = response.data?.requests || []; + + for (const ticket of found) { + const closedAt = toMillis(ticket.completed_time); + if (!closedAt || closedAt <= windowStart) continue; + + const technician = ticket.technician ? ticket.technician.name : null; + if (!technician) continue; + if (technicians.length && !technicians.includes(technician)) continue; + + tickets.set(String(ticket.display_id), { + id: ticket.display_id, + subject: ticket.subject, + technician, + status: ticket.status ? ticket.status.name : null, + closedAt, + closedDisplay: ticket.completed_time?.display_value || null + }); + } + + const cutoff = Date.now() - 12 * 60 * 60 * 1000; + for (const [key, ticket] of tickets) { + if (ticket.closedAt < cutoff) tickets.delete(key); + } + + state.lastCheck = Date.now(); + state.lastError = null; + } catch (err) { + const detail = err.response?.data; + state.lastError = detail?.response_status?.messages?.[0]?.message + || (typeof detail === 'string' ? detail : null) + || err.message; + state.lastCheck = Date.now(); + console.error('Error: ', detail || err.message); + } +} + +async function connect() { + state.auth = 'pending'; + state.lastError = null; + + const deviceData = await getDeviceCode(); + state.verificationUrl = deviceData.verification_uri_complete; + state.userCode = deviceData.user_code; + + console.log('Open this URL and authorize:', deviceData.verification_uri_complete); + console.log('Code: ', deviceData.user_code); + + try { + await pollForToken(deviceData.device_code, deviceData.interval, deviceData.expires_in); + } catch (err) { + state.auth = 'error'; + state.lastError = err.message; + state.verificationUrl = null; + state.userCode = null; + return; + } + + state.auth = 'connected'; + state.verificationUrl = null; + state.userCode = null; + + await fetchClosedTickets(); + setInterval(fetchClosedTickets, CHECK_MINUTES * 60 * 1000); +} + +const app = express(); +app.use(express.static(path.join(__dirname, 'public'))); + +app.get('/api/state', (req, res) => { + res.json({ + ...state, + windowMinutes: WINDOW_MINUTES, + checkMinutes: CHECK_MINUTES, + watching: technicians, + serverTime: Date.now(), + tickets: [...tickets.values()].sort((a, b) => b.closedAt - a.closedAt) + }); +}); + +app.post('/api/connect', async (req, res) => { + if (state.auth === 'pending' || state.auth === 'connected') { + return res.json({ ok: true }); + } + try { + connect(); + res.json({ ok: true }); + } catch (err) { + state.auth = 'error'; + state.lastError = err.message; + res.status(500).json({ ok: false, error: err.message }); + } +}); + +app.post('/api/check', async (req, res) => { + await fetchClosedTickets(); + res.json({ ok: true }); +}); + +app.listen(PORT, () => { + console.log(`Open http://localhost:${PORT}`); +}); diff --git a/src/test.js b/src/test.js deleted file mode 100644 index 4013add..0000000 --- a/src/test.js +++ /dev/null @@ -1,152 +0,0 @@ -//I barely understand this code, so don't ask me to explain it. -// Or do just know it will be very basic explanations. - -require('dotenv').config({ quiet: true }); -const axios = require('axios'); - -const { CLIENT_ID, CLIENT_SECRET, TECHNICIAN_NAMES, STATUS_NAMES } = process.env; - -if (!CLIENT_ID || !CLIENT_SECRET) { - console.error('Client id and secret missing from .env'); - process.exit(1); -} - -const technicians = TECHNICIAN_NAMES ? TECHNICIAN_NAMES.split(',') : []; -const statuses = STATUS_NAMES ? STATUS_NAMES.split(',') : []; - -let accessToken = null; - -async function getDeviceCode() { - const params = new URLSearchParams({ - client_id: CLIENT_ID, - scope: 'SDPOnDemand.requests.READ', - grant_type: 'device_request', - access_type: 'offline' - }); - - const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/code', params); - return res.data; -} - -async function pollForToken(deviceCode, interval) { - const pollParams = new URLSearchParams({ - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - code: deviceCode, - grant_type: 'device_token' - }); - - return new Promise((resolve, reject) => { - const pollInterval = setInterval(async () => { - try { - const res = await axios.post('https://accounts.zoho.com/oauth/v3/device/token', pollParams); - if (res.data.access_token) { - clearInterval(pollInterval); - accessToken = res.data.access_token; - console.log('Authorization successful'); - resolve(); - } - } catch (err) { - - } - }, (interval + 500)); - }); -} - -async function fetchClosedTickets() { - if (!accessToken) return; - - try { - const tenMinutesAgo = Date.now() - (10 * 60 * 1000); - - const listInfo = { - list_info: { - row_count: 50, - sort_field: "completed_time", - sort_order: "desc", - fields_required: ["display_id", "subject", "status", "completed_time", "technician"], - search_criteria: [ - { - field: "status.internal_name", - condition: "is", - values: statuses.map(status => status.trim()) - }, - { - field: "completed_time", - condition: "greater_than", - value: tenMinutesAgo.toString(), - logical_operator: "and" - } - ] - } - }; - - const response = await axios.get( - 'https://ithelp.mainecc.edu/api/v3/requests', - { - params: { input_data: JSON.stringify(listInfo) }, - headers: { - 'Authorization': `Zoho-oauthtoken ${accessToken}`, - 'Accept': 'application/vnd.manageengine.sdp.v3+json' - } - } - ); - - let tickets = response.data?.requests || []; - - const trimmedTechnicians = technicians.map(t => t.trim()); - - tickets = tickets.filter(ticket => { - if (!ticket.completed_time){return false;} - const completedTime = typeof ticket.completed_time === 'object' - ? ticket.completed_time.value - : ticket.completed_time; - if (completedTime <= tenMinutesAgo){return false;} - - const technicianName = ticket.technician ? ticket.technician.name : null; - if (!technicianName){return false;} - return trimmedTechnicians.includes(technicianName); - }); - - console.log(`\nCheck ran at ${new Date().toLocaleTimeString()}`); - - - - - if (tickets.length === 0) { - console.log("No tickets closed in the last 10 minutes."); - } else { - console.log(`Found ${tickets.length} tickets closed since ${new Date(tenMinutesAgo).toLocaleTimeString()}:`); - console.log('---------------------------------------------------------'); - - tickets.forEach((ticket) => { - const ticketId = ticket.display_id; - const subject = ticket.subject; - const tech = ticket.technician ? ticket.technician.name : 'Unassigned'; - const closedAt = ticket.completed_time ? ticket.completed_time.display_value : 'N/A'; - - console.log(`ID: ${ticketId} - ${subject}`); - console.log(`Technician: ${tech}`); - console.log(`Closed At: ${closedAt}`); - console.log('---------------------------------------------------------'); - }); - } - - } catch (err) { - console.error('Error: ', err.response?.data || err.message); - } -} - - -async function main() { - const deviceData = await getDeviceCode(); - console.log('Open this URL and authorize:', deviceData.verification_uri_complete); - console.log('Code: ', deviceData.user_code); - - await pollForToken(deviceData.device_code, deviceData.interval); - - setInterval(fetchClosedTickets, 1 * 60 * 1000); //Only change the first number the first is the amount of minutes between each run - fetchClosedTickets(); -} - -main().catch(err => console.error(err));