diff --git a/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs b/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs
index eafa355..478489f 100644
--- a/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs
+++ b/Refund.Tests/JobQueues/ClusterQueueBatchTests.cs
@@ -78,6 +78,15 @@ public void ParseActiveJobs_AcceptsCommaSeparator(string line, string id, Cluste
Assert.Equal(expected, result[id]);
}
+ [Fact]
+ public void JobResourceValues_IncludeSubmittingUsername()
+ {
+ var job = new MaskJob { SubmittedByUsername = "alice" };
+ var values = job.GetResourceValues();
+
+ Assert.Equal("alice", values["username"]);
+ }
+
[Fact]
public void BuildWorkerScript_PreservesDollarSignsInCommand()
{
diff --git a/Refund/DataModel/Job.cs b/Refund/DataModel/Job.cs
index 7c8c837..f104b7d 100644
--- a/Refund/DataModel/Job.cs
+++ b/Refund/DataModel/Job.cs
@@ -204,6 +204,14 @@ public abstract class Job : RelayBase, IFolderContent
[Clearable]
public User UpdatedBy { get; set; }
+ ///
+ /// Username of the user who queued/submitted this job.
+ /// This is included in cluster submission templates via {{username}}.
+ ///
+ [Clearable]
+ [RelayProperty]
+ public string SubmittedByUsername { get; set; } = string.Empty;
+
///
/// User-provided notes or comments about this job.
///
@@ -430,7 +438,8 @@ public Dictionary GetResourceValues()
{ "gpu_memory_gb", GpuMemoryGb.ToString() },
{ "run_directory", RunDirectory },
{ "std_out", PathStdOut },
- { "std_err", PathStdErr }
+ { "std_err", PathStdErr },
+ { "username", SubmittedByUsername }
};
}
diff --git a/Refund/JobQueues/ClusterQueue.cs b/Refund/JobQueues/ClusterQueue.cs
index 53435b3..4c08394 100644
--- a/Refund/JobQueues/ClusterQueue.cs
+++ b/Refund/JobQueues/ClusterQueue.cs
@@ -516,7 +516,7 @@ await job.WriteToLifecycleLog(
await job.WriteToLifecycleLog($"Submitting script: {scriptPath}");
string rawOutput = null;
- string jobId = await SubmitScript(scriptPath, o => rawOutput = o);
+ string jobId = await SubmitScript(scriptPath, o => rawOutput = o, job.SubmittedByUsername);
await job.WriteToLifecycleLog(rawOutput);
await job.WriteToLifecycleLog($"Parsed cluster job ID: {jobId}");
@@ -684,9 +684,12 @@ private async Task PrepareAndWriteScript(Job job, Dictionary
- public async Task SubmitScript(string scriptPath, Action onRawOutput = null)
+ public async Task SubmitScript(string scriptPath, Action onRawOutput = null, string username = "")
{
- string clusterCommand = SubmitJobTemplate.ReplaceRegex("{{\\s*script_path_abs\\s*}}", scriptPath);
+ string clusterCommand = SubmitJobTemplate
+ .ReplaceRegex("{{\\s*script_path_abs\\s*}}", scriptPath)
+ .ReplaceRegex("{{\\s*username\\s*}}", username ?? string.Empty);
+
string output = await ExecuteOnCluster(clusterCommand);
onRawOutput?.Invoke(output);
return ParseClusterJobId(output);
diff --git a/Refund/Services/Core/DataManager/DataManager.Job.cs b/Refund/Services/Core/DataManager/DataManager.Job.cs
index 043fd6a..5f4d996 100644
--- a/Refund/Services/Core/DataManager/DataManager.Job.cs
+++ b/Refund/Services/Core/DataManager/DataManager.Job.cs
@@ -613,6 +613,7 @@ await ExecuteWithLock(async () =>
{
j.Status = JobStatus.Waiting;
j.QueueId = originalQueue.Id;
+ j.SubmittedByUsername = user.Username;
j.AddEvent(EventType.WaitingStarted, originalUser);
});
diff --git a/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs b/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs
index e5f63cc..b5faa74 100644
--- a/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs
+++ b/Relay/Screens/Overlay/Settings/QueueEditor.razor.cs
@@ -181,6 +181,7 @@ private IEnumerable GetSubmissionScriptVariables()
var variables = new List
{
"command",
+ "username",
"job_id",
"n_processes",
"n_cores",
@@ -205,7 +206,7 @@ private IEnumerable GetCommandTemplateVariables()
private IEnumerable GetSubmitJobTemplateVariables()
{
- return ["script_path_abs"];
+ return ["script_path_abs", "username"];
}
private IEnumerable GetStatusJobTemplateVariables()