Add USB checkout system and SSO profile page

New Features:
- USB Device checkout/check-in system with barcode scanning
  - displayusb.asp: List all USB devices with status
  - addusb.asp: Add new USB devices via barcode scan
  - checkout_usb.asp/savecheckout_usb.asp: Check out USB to SSO
  - checkin_usb.asp/savecheckin_usb.asp: Check in with wipe confirmation
  - usb_history.asp: Full checkout history with filters
  - api_usb.asp: JSON API for AJAX lookups
- displayprofile.asp: SSO profile page showing user info and USB history
- Date/time format changed to 12-hour (MM/DD/YYYY h:mm AM/PM)
- SSO links in USB history now link to profile page via search

Database:
- New machinetypeid 44 for USB devices
- New usb_checkouts table for tracking checkouts

Cleanup:
- Removed v2 folder (duplicate/old files)
- Removed old debug/test files
- Removed completed migration documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
cproudlock
2025-12-07 11:16:14 -05:00
parent c7834d4b99
commit 65b622c361
1061 changed files with 19034 additions and 213120 deletions

76
tv-dashboard/README.txt Normal file
View File

@@ -0,0 +1,76 @@
TV Dashboard - Slide Display
============================
SETUP
-----
1. Open the dashboard in a browser on the TV/display PC
2. Click the gear icon (top right) to open settings
3. Set the base path (default: S:\ProcessData\CommDisplay\ShopSS)
4. Optionally set a subfolder (e.g., Christmas2025)
5. Click "Apply & Start"
ADDING SLIDES
-------------
Due to browser security, the dashboard cannot list directory contents directly.
Use ONE of these methods:
METHOD 1: slides.txt file (RECOMMENDED)
- Create a file named "slides.txt" in your slides folder
- List each image filename on its own line
- Example:
001.jpg
002.jpg
003.jpg
company_logo.png
safety_message.jpg
METHOD 2: Numbered files
- Name your images with 3-digit numbers: 001.jpg, 002.jpg, 003.jpg, etc.
- The dashboard will automatically find them in order
METHOD 3: Slide1, Slide2, etc.
- Name files Slide1.jpg, Slide2.jpg, Slide3.jpg, etc.
- Common when exporting from PowerPoint
CREATING SUBFOLDERS
-------------------
To organize different presentations:
S:\ProcessData\CommDisplay\ShopSS\
├── slides.txt <- default slides
├── 001.jpg
├── 002.jpg
├── Christmas2025\
│ ├── slides.txt
│ ├── holiday_01.jpg
│ └── holiday_02.jpg
└── Safety\
├── slides.txt
└── safety_001.jpg
Then in settings, enter the subfolder name (e.g., "Christmas2025")
KEYBOARD SHORTCUTS
------------------
Space - Pause/Resume slideshow
Left Arrow - Previous slide
Right Arrow - Next slide
S - Toggle settings panel
F - Toggle fullscreen
R - Reload slides
SUPPORTED IMAGE FORMATS
-----------------------
jpg, jpeg, png, gif, bmp, webp
TIPS
----
- Press F11 in the browser for fullscreen
- Settings are saved in the browser and persist after refresh
- The settings gear only appears when you move the mouse
- Hover to see the status bar with current slide number

View File

@@ -0,0 +1,78 @@
<%@ Language="VBScript" %>
<%
Response.ContentType = "application/json"
Response.Buffer = True
' Slides folder path (local to IIS)
Dim SLIDES_FOLDER
SLIDES_FOLDER = Server.MapPath("slides")
' Valid image extensions
Dim validExtensions
validExtensions = Array("jpg", "jpeg", "png", "gif", "bmp", "webp")
On Error Resume Next
Dim fso, folder, file, slides, ext, i
Set fso = Server.CreateObject("Scripting.FileSystemObject")
' Check if folder exists
If Not fso.FolderExists(SLIDES_FOLDER) Then
Response.Write("{""success"":false,""message"":""Slides folder not found""}")
Set fso = Nothing
Response.End
End If
Set folder = fso.GetFolder(SLIDES_FOLDER)
' Build array of image files
Dim fileList()
Dim fileCount
fileCount = 0
For Each file In folder.Files
ext = LCase(fso.GetExtensionName(file.Name))
' Check if valid image extension
For i = 0 To UBound(validExtensions)
If ext = validExtensions(i) Then
ReDim Preserve fileList(fileCount)
fileList(fileCount) = file.Name
fileCount = fileCount + 1
Exit For
End If
Next
Next
' Sort files alphabetically (simple bubble sort)
Dim j, temp
If fileCount > 1 Then
For i = 0 To fileCount - 2
For j = i + 1 To fileCount - 1
If fileList(i) > fileList(j) Then
temp = fileList(i)
fileList(i) = fileList(j)
fileList(j) = temp
End If
Next
Next
End If
' Build JSON response
Dim json
json = "{""success"":true,""basepath"":""slides/"",""slides"":["
If fileCount > 0 Then
For i = 0 To fileCount - 1
If i > 0 Then json = json & ","
json = json & "{""filename"":""" & fileList(i) & """}"
Next
End If
json = json & "]}"
Response.Write(json)
Set folder = Nothing
Set fso = Nothing
%>

BIN
tv-dashboard/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

179
tv-dashboard/index.html Normal file
View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>West Jefferson - Display</title>
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
width: 100vw;
}
.slideshow-container {
position: relative;
width: 100vw;
height: 100vh;
background: #000;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 1s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.slide.active {
opacity: 1;
}
.slide img {
width: 100%;
height: 100%;
object-fit: contain;
}
.progress-bar {
position: fixed;
bottom: 0;
left: 0;
height: 4px;
background: #4181ff;
transition: width linear;
z-index: 100;
}
.error-message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 24px;
}
.error-message h2 {
color: #ff4444;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="slideshow-container" id="slideshow"></div>
<div class="progress-bar" id="progressBar"></div>
<script>
const INTERVAL = 10; // seconds between slides
let slides = [];
let currentSlide = 0;
let slideTimer = null;
// Fetch slides from API
async function fetchSlides() {
try {
const response = await fetch('api_slides.asp');
if (!response.ok) throw new Error('API error');
const data = await response.json();
if (data.success && data.slides && data.slides.length > 0) {
updateSlides(data.slides, data.basepath);
} else {
showError(data.message || 'No slides found');
}
} catch (error) {
console.error('Error fetching slides:', error);
showError('Unable to load slides');
}
}
// Update slides in DOM
function updateSlides(newSlides, basepath) {
const slideshow = document.getElementById('slideshow');
const currentSlideNames = slides.map(s => s.filename);
const newSlideNames = newSlides.map(s => s.filename);
// Check if slides changed
if (JSON.stringify(currentSlideNames) !== JSON.stringify(newSlideNames)) {
slideshow.innerHTML = '';
slides = [];
newSlides.forEach((slide, index) => {
const div = document.createElement('div');
div.className = 'slide' + (index === 0 ? ' active' : '');
const img = document.createElement('img');
img.src = basepath + encodeURIComponent(slide.filename);
div.appendChild(img);
slideshow.appendChild(div);
slides.push({ element: div, filename: slide.filename });
});
currentSlide = 0;
if (slides.length > 1) startSlideshow();
}
}
function showError(message) {
document.getElementById('slideshow').innerHTML =
'<div class="error-message"><h2>Display Error</h2><p>' + message + '</p></div>';
}
function startSlideshow() {
if (slideTimer) clearTimeout(slideTimer);
scheduleNextSlide();
}
function scheduleNextSlide() {
const progressBar = document.getElementById('progressBar');
progressBar.style.width = '0%';
progressBar.style.transition = 'none';
setTimeout(() => {
progressBar.style.transition = 'width ' + INTERVAL + 's linear';
progressBar.style.width = '100%';
}, 50);
slideTimer = setTimeout(() => {
nextSlide();
scheduleNextSlide();
}, INTERVAL * 1000);
}
function nextSlide() {
if (slides.length === 0) return;
slides[currentSlide].element.classList.remove('active');
currentSlide = (currentSlide + 1) % slides.length;
slides[currentSlide].element.classList.add('active');
}
// Start
fetchSlides();
// Refresh every 60 seconds to pick up new slides
setInterval(fetchSlides, 60000);
</script>
</body>
</html>

61
tv-dashboard/slide.asp Normal file
View File

@@ -0,0 +1,61 @@
<%@ Language="VBScript" %>
<%
Response.Buffer = True
' Slides folder path (UNC)
Const SLIDES_FOLDER = "\\tsgwp00525.rd.ds.ge.com\shared\dt\tv"
' Get filename from querystring
Dim filename, filepath, fso, ext
filename = Request.QueryString("file")
' Validate filename - no path traversal
If InStr(filename, "..") > 0 Or InStr(filename, "/") > 0 Or InStr(filename, "\") > 0 Or filename = "" Then
Response.Status = "400 Bad Request"
Response.End
End If
filepath = SLIDES_FOLDER & "\" & filename
Set fso = Server.CreateObject("Scripting.FileSystemObject")
' Check file exists
If Not fso.FileExists(filepath) Then
Response.Status = "404 Not Found"
Set fso = Nothing
Response.End
End If
' Get extension and set content type
ext = LCase(fso.GetExtensionName(filename))
Select Case ext
Case "jpg", "jpeg"
Response.ContentType = "image/jpeg"
Case "png"
Response.ContentType = "image/png"
Case "gif"
Response.ContentType = "image/gif"
Case "bmp"
Response.ContentType = "image/bmp"
Case "webp"
Response.ContentType = "image/webp"
Case Else
Response.Status = "415 Unsupported Media Type"
Set fso = Nothing
Response.End
End Select
Set fso = Nothing
' Read and serve the file
Dim stream
Set stream = Server.CreateObject("ADODB.Stream")
stream.Type = 1 ' Binary
stream.Open
stream.LoadFromFile filepath
Response.BinaryWrite stream.Read
stream.Close
Set stream = Nothing
%>

137
tv-dashboard/slideshow.html Normal file
View File

@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>West Jefferson - Display</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
overflow: hidden;
height: 100vh;
width: 100vw;
}
.slide {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
opacity: 0;
transition: opacity 1s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
}
.slide.active { opacity: 1; }
.slide img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.progress-bar {
position: fixed;
bottom: 0; left: 0;
height: 4px;
background: #4181ff;
z-index: 100;
}
.status {
position: fixed;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-family: Arial, sans-serif;
font-size: 24px;
text-align: center;
}
</style>
</head>
<body>
<div id="slideshow"><div class="status">Loading slides...</div></div>
<div class="progress-bar" id="progressBar"></div>
<!-- Slides list generated by update_slides.bat -->
<script>var SLIDES = [];</script>
<script src="slides.js"></script>
<script>
const INTERVAL = 10;
let slides = [];
let currentSlide = 0;
let slideTimer = null;
function loadSlides() {
if (typeof SLIDES === 'undefined' || SLIDES.length === 0) {
document.getElementById('slideshow').innerHTML =
'<div class="status">No slides found.<br><br><span style="font-size:16px;color:#888;">Run update_slides.bat to generate slides.js</span></div>';
return;
}
buildSlides(SLIDES);
}
function buildSlides(filenames) {
const container = document.getElementById('slideshow');
container.innerHTML = '';
slides = [];
filenames.forEach((filename, i) => {
const div = document.createElement('div');
div.className = 'slide' + (i === 0 ? ' active' : '');
const img = document.createElement('img');
img.src = filename;
div.appendChild(img);
container.appendChild(div);
slides.push({ element: div, filename: filename });
});
currentSlide = 0;
if (slides.length > 1) startSlideshow();
}
function startSlideshow() {
if (slideTimer) clearTimeout(slideTimer);
scheduleNext();
}
function scheduleNext() {
const bar = document.getElementById('progressBar');
bar.style.transition = 'none';
bar.style.width = '0%';
setTimeout(() => {
bar.style.transition = 'width ' + INTERVAL + 's linear';
bar.style.width = '100%';
}, 50);
slideTimer = setTimeout(() => {
nextSlide();
scheduleNext();
}, INTERVAL * 1000);
}
function nextSlide() {
if (slides.length === 0) return;
slides[currentSlide].element.classList.remove('active');
currentSlide = (currentSlide + 1) % slides.length;
slides[currentSlide].element.classList.add('active');
}
let lastSlideList = JSON.stringify(SLIDES);
loadSlides();
// Check for new slides every 60 seconds
setInterval(function() {
const script = document.createElement('script');
script.src = 'slides.js?t=' + Date.now();
script.onload = function() {
const newList = JSON.stringify(SLIDES);
if (newList !== lastSlideList) {
lastSlideList = newList;
loadSlides();
}
};
document.head.appendChild(script);
}, 60000);
</script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@echo off
:: TV Slideshow - Generate slides.js
:: Run this after adding/removing slides
cd /d "S:\DT\tv"
:: Generate slides.js from image files in slides subfolder
echo var SLIDES = [ > slides.js
setlocal enabledelayedexpansion
set first=1
for /f "tokens=*" %%F in ('dir /b /on slides\*.jpg slides\*.jpeg slides\*.png slides\*.gif slides\*.bmp slides\*.webp 2^>nul') do (
if !first!==1 (
echo "slides/%%F" >> slides.js
set first=0
) else (
echo ,"slides/%%F" >> slides.js
)
)
endlocal
echo ]; >> slides.js
echo Updated slides.js
pause