Files
claudetools/clients/dataforth/session-manager/Default.aspx
Mike Swanson fe3b5b0382 Add SAGE-SQL session manager app, shared work items board, update session log
- Session manager: self-service RDP session reset for Dataforth users (Default.aspx + web.config)
- WORKITEMS.md: shared task board for Mike/Howard with @tagging, syncs via Gitea
- Session log: deployment deferred due to VPN connectivity issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 20:05:54 -07:00

382 lines
13 KiB
Plaintext

<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Text.RegularExpressions" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Web.Security" %>
<script runat="server">
public class SessionInfo
{
public string SessionName { get; set; }
public string Username { get; set; }
public int Id { get; set; }
public string State { get; set; }
public bool CanReset { get; set; }
}
private string currentUser;
private string currentDisplayName;
private List<SessionInfo> userSessions = new List<SessionInfo>();
private string statusMessage = "";
private string statusClass = "";
protected void Page_Load(object sender, EventArgs e)
{
currentUser = Context.User.Identity.Name;
// Strip domain prefix
if (currentUser.Contains("\\"))
currentUser = currentUser.Split('\\')[1];
currentDisplayName = currentUser;
// Handle reset action
string resetId = Request.QueryString["reset"];
if (!string.IsNullOrEmpty(resetId))
{
int sessionId;
if (int.TryParse(resetId, out sessionId))
{
// Verify the session belongs to this user before resetting
var sessions = GetSessions();
var target = sessions.Find(s => s.Id == sessionId &&
s.Username.Equals(currentUser, StringComparison.OrdinalIgnoreCase));
if (target != null && target.State == "Disc")
{
RunCommand("logoff " + sessionId);
LogAction(currentUser, sessionId, "reset");
statusMessage = "Session #" + sessionId + " has been reset.";
statusClass = "success";
}
else if (target != null)
{
statusMessage = "Only disconnected sessions can be reset.";
statusClass = "warning";
}
else
{
statusMessage = "Session not found or does not belong to you.";
statusClass = "error";
}
}
// Redirect to clean URL after action
Response.Redirect(Request.Path + "?done=" + Server.UrlEncode(statusMessage) +
"&class=" + statusClass);
return;
}
// Check for post-redirect message
if (!string.IsNullOrEmpty(Request.QueryString["done"]))
{
statusMessage = Request.QueryString["done"];
statusClass = Request.QueryString["class"] ?? "success";
}
userSessions = GetSessions().FindAll(s =>
s.Username.Equals(currentUser, StringComparison.OrdinalIgnoreCase));
}
private List<SessionInfo> GetSessions()
{
var sessions = new List<SessionInfo>();
string output = RunCommand("query session");
if (string.IsNullOrEmpty(output)) return sessions;
foreach (string line in output.Split('\n'))
{
string trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("SESSIONNAME"))
continue;
// Parse the fixed-width output from query session
// Format varies — use positional parsing
var match = Regex.Match(trimmed,
@"^[> ]*([\w-#]*)\s{2,}(\w+)\s+(\d+)\s+(\w+)");
if (match.Success)
{
sessions.Add(new SessionInfo
{
SessionName = match.Groups[1].Value.Trim(),
Username = match.Groups[2].Value.Trim(),
Id = int.Parse(match.Groups[3].Value),
State = match.Groups[4].Value.Trim(),
CanReset = match.Groups[4].Value.Trim() == "Disc"
});
}
else
{
// Try alternate format (no session name, just username)
match = Regex.Match(trimmed, @"^\s+(\w+)\s+(\d+)\s+(\w+)");
if (match.Success)
{
sessions.Add(new SessionInfo
{
SessionName = "",
Username = match.Groups[1].Value.Trim(),
Id = int.Parse(match.Groups[2].Value),
State = match.Groups[3].Value.Trim(),
CanReset = match.Groups[3].Value.Trim() == "Disc"
});
}
}
}
return sessions;
}
private string RunCommand(string cmd)
{
try
{
var psi = new ProcessStartInfo("cmd.exe", "/c " + cmd)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
var proc = Process.Start(psi);
string output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit(10000);
return output;
}
catch { return ""; }
}
private void LogAction(string user, int sessionId, string action)
{
try
{
string logPath = Server.MapPath("~/logs");
if (!System.IO.Directory.Exists(logPath))
System.IO.Directory.CreateDirectory(logPath);
string logFile = System.IO.Path.Combine(logPath,
DateTime.Now.ToString("yyyy-MM") + ".log");
System.IO.File.AppendAllText(logFile,
string.Format("{0} | {1} | session {2} | {3}\r\n",
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
user, sessionId, action));
}
catch { }
}
</script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Session Manager — SAGE-SQL</title>
<style>
:root {
--bg: #0f1923;
--surface: #172a3a;
--surface2: #1e3448;
--border: #2a4a5e;
--text: #e0e8f0;
--text-muted: #7a9ab5;
--accent: #38bdf8;
--accent-hover: #7dd3fc;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--disc: #f59e0b;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
justify-content: center;
padding: 40px 20px;
}
.container {
max-width: 720px;
width: 100%;
}
.header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 32px;
}
.header-icon {
width: 48px; height: 48px;
background: var(--accent);
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 24px; font-weight: 700; color: var(--bg);
}
.header h1 {
font-size: 22px; font-weight: 600;
color: var(--text);
}
.header p {
font-size: 14px; color: var(--text-muted);
margin-top: 2px;
}
.welcome {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px 20px;
margin-bottom: 24px;
display: flex; align-items: center; justify-content: space-between;
}
.welcome-user {
font-size: 15px; color: var(--text);
}
.welcome-user strong { color: var(--accent); }
.refresh-btn {
background: var(--surface2);
border: 1px solid var(--border);
color: var(--text-muted);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
text-decoration: none;
transition: all 0.15s;
}
.refresh-btn:hover {
background: var(--border);
color: var(--text);
}
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
border: 1px solid;
}
.alert.success { background: rgba(34,197,94,0.1); border-color: var(--success); color: var(--success); }
.alert.warning { background: rgba(245,158,11,0.1); border-color: var(--warning); color: var(--warning); }
.alert.error { background: rgba(239,68,68,0.1); border-color: var(--danger); color: var(--danger); }
.sessions-table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
.sessions-table th {
text-align: left;
padding: 12px 16px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
background: var(--surface2);
border-bottom: 1px solid var(--border);
}
.sessions-table td {
padding: 14px 16px;
font-size: 14px;
border-bottom: 1px solid var(--border);
}
.sessions-table tr:last-child td { border-bottom: none; }
.state-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.state-active { background: rgba(34,197,94,0.15); color: var(--success); }
.state-disc { background: rgba(245,158,11,0.15); color: var(--disc); }
.reset-btn {
background: var(--danger);
color: white;
border: none;
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
transition: opacity 0.15s;
}
.reset-btn:hover { opacity: 0.85; }
.no-sessions {
text-align: center;
padding: 48px 20px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
}
.no-sessions p { color: var(--text-muted); font-size: 15px; }
.no-sessions .check { font-size: 40px; margin-bottom: 12px; color: var(--success); }
.footer {
text-align: center;
margin-top: 32px;
font-size: 12px;
color: var(--text-muted);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-icon">S</div>
<div>
<h1>Session Manager</h1>
<p>SAGE-SQL &mdash; Reset disconnected RemoteApp sessions</p>
</div>
</div>
<div class="welcome">
<span class="welcome-user">Signed in as <strong><%= currentDisplayName %></strong></span>
<a href="<%= Request.Path %>" class="refresh-btn">Refresh</a>
</div>
<% if (!string.IsNullOrEmpty(statusMessage)) { %>
<div class="alert <%= statusClass %>"><%= Server.HtmlEncode(statusMessage) %></div>
<% } %>
<% if (userSessions.Count > 0) { %>
<table class="sessions-table">
<thead>
<tr>
<th>Session</th>
<th>State</th>
<th>ID</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<% foreach (var s in userSessions) { %>
<tr>
<td><%= string.IsNullOrEmpty(s.SessionName) ? "RemoteApp" : Server.HtmlEncode(s.SessionName) %></td>
<td>
<span class="state-badge <%= s.State == "Disc" ? "state-disc" : "state-active" %>">
<%= s.State == "Disc" ? "Disconnected" : s.State %>
</span>
</td>
<td>#<%= s.Id %></td>
<td>
<% if (s.CanReset) { %>
<a href="?reset=<%= s.Id %>" class="reset-btn"
onclick="return confirm('Reset session #<%= s.Id %>?')">Reset</a>
<% } else { %>
<span style="color: var(--text-muted); font-size: 13px;">Active</span>
<% } %>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } else { %>
<div class="no-sessions">
<div class="check">&#10003;</div>
<p>No sessions found for your account.<br>You're all clear.</p>
</div>
<% } %>
<div class="footer">
Dataforth Corporation &mdash; IT Services by AZ Computer Guru
</div>
</div>
</body>
</html>