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:
76
tv-dashboard/README.txt
Normal file
76
tv-dashboard/README.txt
Normal 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
|
||||
78
tv-dashboard/api_slides.asp
Normal file
78
tv-dashboard/api_slides.asp
Normal 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
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
179
tv-dashboard/index.html
Normal 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
61
tv-dashboard/slide.asp
Normal 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
137
tv-dashboard/slideshow.html
Normal 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>
|
||||
23
tv-dashboard/update_slides.bat
Normal file
23
tv-dashboard/update_slides.bat
Normal 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
|
||||
Reference in New Issue
Block a user