From 918a0bfda1492989ccd0efc54b0027358bdaf3d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 09:39:11 +0100
Subject: [PATCH 01/28] =?UTF-8?q?=F0=9F=94=A7=20local=20build=20builds=20p?=
 =?UTF-8?q?roject=20only,=20not=20the=20whole=20solution?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 _local_build.cmd | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 _local_build.cmd

diff --git a/_local_build.cmd b/_local_build.cmd
new file mode 100644
index 0000000..cad8fb4
--- /dev/null
+++ b/_local_build.cmd
@@ -0,0 +1,3 @@
+pushd jwl.console
+call global_build.cmd jira-worklogger
+@exit /b %ERRORLEVEL%

From ef28b904cb1c9a1eaa4555254ab47555dcba42a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 09:45:09 +0100
Subject: [PATCH 02/28] =?UTF-8?q?=F0=9F=94=A7=20fixed=20example=20config?=
 =?UTF-8?q?=20file=20&=20readme=20for=20changed=20server=20flavours?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                   | 6 +++---
 jwl.core/_assets/jwl.config | 2 +-
 jwl.core/jwl.core.csproj    | 8 ++++++++
 3 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index c5d6eb1..a33d709 100644
--- a/README.md
+++ b/README.md
@@ -28,9 +28,9 @@ As for the CLI worklogger binary, there are command-line options available as we
 ### "ServerClass" setting
 
 Available values are:
- - VanillaJira
- - TempoTimeSheetsPlugin
- - ICTimePlugin (not implemented yet)
+ - Vanilla
+ - TempoTimeSheets
+ - ICTime (not implemented yet)
 
 ## The input CSV structure
 
diff --git a/jwl.core/_assets/jwl.config b/jwl.core/_assets/jwl.config
index ce7889f..7ab44d9 100644
--- a/jwl.core/_assets/jwl.config
+++ b/jwl.core/_assets/jwl.config
@@ -1,7 +1,7 @@
 {
 	"UseVerboseFeedback": false,
 	"JiraServer": {
-		"ServerClass": "VanillaJira",
+		"ServerClass": "Vanilla",
 		"MaxConnectionsPerServer": 4,
 		"UseProxy": false,
 		"SkipSslCertificateCheck": false
diff --git a/jwl.core/jwl.core.csproj b/jwl.core/jwl.core.csproj
index b0a6a15..00bae86 100644
--- a/jwl.core/jwl.core.csproj
+++ b/jwl.core/jwl.core.csproj
@@ -53,6 +53,14 @@
     <CommonAssetFiles Include="../*.md" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>
 
+  <ItemGroup>
+    <CommonAssetFiles Remove="..\README.md" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Include="..\README.md" Link="_assets\README.md" />
+  </ItemGroup>
+
   <Target Name="CopyCustomContent" AfterTargets="AfterBuild">
     <Copy SourceFiles="@(CommonAssetFiles)" DestinationFolder="$(OutDir)/%(RecursiveDir)" SkipUnchangedFiles="true" />
   </Target>

From 8d1701f82f2af7683131f3ad5af8fb172371a405 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 10:38:20 +0100
Subject: [PATCH 03/28] =?UTF-8?q?=F0=9F=94=A7=20core=20process=20now=20rel?=
 =?UTF-8?q?ies=20on=20JJiraServerApi=20interface=20instead=20of=20vanilla?=
 =?UTF-8?q?=20jira=20class?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.core/JwlCoreProcess.cs | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/jwl.core/JwlCoreProcess.cs b/jwl.core/JwlCoreProcess.cs
index f76420b..e7389a9 100644
--- a/jwl.core/JwlCoreProcess.cs
+++ b/jwl.core/JwlCoreProcess.cs
@@ -21,7 +21,7 @@ public class JwlCoreProcess : IDisposable
     private AppConfig _config;
     private HttpClientHandler _httpClientHandler;
     private HttpClient _httpClient;
-    private VanillaJiraServerApi _jiraClient;
+    private IJiraServerApi _jiraClient;
 
     private jwl.jira.api.rest.common.JiraUserInfo? _userInfo;
     private Dictionary<string, WorkLogType> availableWorklogTypes = new ();
@@ -44,8 +44,10 @@ public JwlCoreProcess(AppConfig config, ICoreProcessInteraction interaction)
         _httpClient = new HttpClient(_httpClientHandler)
         {
             BaseAddress = new Uri(_config.JiraServer?.BaseUrl ?? string.Empty)
-        };
-        _jiraClient = new VanillaJiraServerApi(_httpClient, _config.User?.Name ?? throw new ArgumentNullException($"{nameof(_config)}.{nameof(_config.User)}.{nameof(_config.User.Name)})"));
+        };
+
+        string userName = _config.User?.Name ?? throw new ArgumentNullException($"{nameof(_config)}.{nameof(_config.User)}.{nameof(_config.User.Name)})");
+        _jiraClient = ServerApiFactory.CreateApi(_httpClient, userName, _config.JiraServer?.ServerFlavourId ?? JiraServerFlavour.Vanilla);
 
         /* 2do!...
         _jiraClient.WsClient.HttpRequestPostprocess = req =>
@@ -154,7 +156,7 @@ private async Task FillJiraWithWorklogs(InputWorkLog[] inputWorklogs, WorkLog[]
         Task[] fillJiraWithWorklogsTasks = worklogsForDeletion
             .Select(worklog => _jiraClient.DeleteWorklog(worklog.IssueId, worklog.Id))
             .Concat(inputWorklogs
-                .Select(worklog => _jiraClient.AddWorklog(worklog.IssueKey.ToString(), DateOnly.FromDateTime(worklog.Date), (int)worklog.TimeSpent.TotalSeconds, worklog.TempWorklogType, string.Empty))
+                .Select(worklog => _jiraClient.AddWorklog(worklog.IssueKey.ToString(), DateOnly.FromDateTime(worklog.Date), (int)worklog.TimeSpent.TotalSeconds, worklog.WorkLogActivity, string.Empty))
             )
             .ToArray();
 
@@ -205,8 +207,8 @@ private async Task<InputWorkLog[]> ReadInputFile(string fileName)
             return worklogReader
                 .Read(row =>
                 {
-                    if (!availableWorklogTypes.ContainsKey(row.TempWorklogType))
-                        throw new InvalidDataException($"Worklog type {row.TempWorklogType} not found on server");
+                    if (!availableWorklogTypes.ContainsKey(row.WorkLogActivity))
+                        throw new InvalidDataException($"Worklog type {row.WorkLogActivity} not found on server");
                 })
                 .ToArray();
         });

From 4b5055e486ec7af0cb5da3439843203b8145b79e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 10:40:19 +0100
Subject: [PATCH 04/28] =?UTF-8?q?=F0=9F=94=A7=20input=20fields=20naming=20?=
 =?UTF-8?q?refactored?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.inputs/InputWorkLog.cs      | 4 ++--
 jwl.inputs/JiraWorklogRawCsv.cs | 4 ++--
 jwl.inputs/WorklogCsvReader.cs  | 5 +++--
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/jwl.inputs/InputWorkLog.cs b/jwl.inputs/InputWorkLog.cs
index b95da9b..5d259f5 100644
--- a/jwl.inputs/InputWorkLog.cs
+++ b/jwl.inputs/InputWorkLog.cs
@@ -6,6 +6,6 @@ public struct InputWorkLog
     public JiraIssueKey IssueKey;
     public DateTime Date;
     public TimeSpan TimeSpent;
-    public string TempWorklogType;
-    public string Comment;
+    public string WorkLogActivity;
+    public string WorkLogComment;
 }
diff --git a/jwl.inputs/JiraWorklogRawCsv.cs b/jwl.inputs/JiraWorklogRawCsv.cs
index 912a547..98a34fe 100644
--- a/jwl.inputs/JiraWorklogRawCsv.cs
+++ b/jwl.inputs/JiraWorklogRawCsv.cs
@@ -6,7 +6,7 @@ public record JiraWorklogRawCsv
     string IssueKey,
     string Date,
     string TimeSpent,
-    string TempoWorklogType,
-    string Comment
+    string WorkLogActivity,
+    string WorkLogComment
 )
 { }
diff --git a/jwl.inputs/WorklogCsvReader.cs b/jwl.inputs/WorklogCsvReader.cs
index e0961d6..443377a 100644
--- a/jwl.inputs/WorklogCsvReader.cs
+++ b/jwl.inputs/WorklogCsvReader.cs
@@ -37,6 +37,7 @@ public IEnumerable<InputWorkLog> Read(Action<InputWorkLog>? postProcessResult =
 
         string[] timespanTimeFormats =
         {
+            @"hh\:mm\:ss",
             @"hh\:mm",
             @"mm",
             @"hh'h'mm",
@@ -69,8 +70,8 @@ public IEnumerable<InputWorkLog> Read(Action<InputWorkLog>? postProcessResult =
                     IssueKey = worklogIssueKey,
                     Date = worklogDate,
                     TimeSpent = worklogTimeSpent,
-                    TempWorklogType = row.TempoWorklogType,
-                    Comment = row.Comment
+                    WorkLogActivity = row.WorkLogActivity,
+                    WorkLogComment = row.WorkLogComment
                 };
             }
             catch (Exception e)

From 91824e20214229bb29a51112a2b19d1215c8dd32 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 10:41:11 +0100
Subject: [PATCH 05/28] =?UTF-8?q?=F0=9F=94=A7=20fix:=20retrieval=20of=20wo?=
 =?UTF-8?q?rklogs=20per=20user,=20issue=20and=20date=20range=20was=20missi?=
 =?UTF-8?q?ng=20the=20user=20predicate?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/VanillaJiraServerApi.cs              | 26 ++++++++++---------
 .../rest/request/JiraAddWorklogByIssueKey.cs  |  6 ++---
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/jwl.jira/VanillaJiraServerApi.cs b/jwl.jira/VanillaJiraServerApi.cs
index 646e7d9..45036cf 100644
--- a/jwl.jira/VanillaJiraServerApi.cs
+++ b/jwl.jira/VanillaJiraServerApi.cs
@@ -3,6 +3,7 @@ namespace jwl.jira;
 using System.Collections.Generic;
 using System.Net.Http;
 using System.Net.Http.Json;
+using System.Xml.Linq;
 using jwl.infra;
 
 public class VanillaJiraServerApi
@@ -58,6 +59,7 @@ public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnume
 
         var result = responseTasks
             .SelectMany(task => task.Result.Worklogs)
+            .Where(worklog => worklog.Author.Name == UserName)
             .Where(worklog => worklog.Started.Value >= minDt && worklog.Started.Value < supDt)
             .Select(wl => new WorkLog(
                 Id: wl.Id.Value,
@@ -83,15 +85,15 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
                 .Add(issueKey)
                 .Add(@"worklog")
         };
-        var request = new api.rest.request.JiraAddWorklogByIssueKey()
-        {
-            Started = day
+        var request = new api.rest.request.JiraAddWorklogByIssueKey(
+            Started: day
+                .ToDateTime(TimeOnly.MinValue)
                 .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
                 .Replace(":", string.Empty)
                 .Replace(';', ':'),
-            TimeSpentSeconds = timeSpentSeconds,
-            Comment = comment
-        };
+            TimeSpentSeconds: timeSpentSeconds,
+            Comment: comment
+        );
         await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
     }
 
@@ -137,15 +139,15 @@ public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, i
                 .Add(@"worklog")
                 .Add(worklogId.ToString())
         };
-        var request = new api.rest.request.JiraAddWorklogByIssueKey()
-        {
-            Started = day
+        var request = new api.rest.request.JiraAddWorklogByIssueKey(
+            Started: day
+                .ToDateTime(TimeOnly.MinValue)
                 .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
                 .Replace(":", string.Empty)
                 .Replace(';', ':'),
-            TimeSpentSeconds = timeSpentSeconds,
-            Comment = comment
-        };
+            TimeSpentSeconds: timeSpentSeconds,
+            Comment: comment
+        );
         await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
     }
 }
diff --git a/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs b/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs
index ee04cb4..035be2c 100644
--- a/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs
+++ b/jwl.jira/api/rest/request/JiraAddWorklogByIssueKey.cs
@@ -1,8 +1,6 @@
+#pragma warning disable SA1313
 namespace jwl.jira.api.rest.request;
 
-public class JiraAddWorklogByIssueKey
+public record JiraAddWorklogByIssueKey(string Started, int TimeSpentSeconds, string? Comment)
 {
-    public string? Started { get; init; }
-    public int? TimeSpentSeconds { get; init; }
-    public string? Comment { get; init; }
 }

From e8ef1fd0aa37591f1f3a5942b67d2633d10833f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 10:45:45 +0100
Subject: [PATCH 06/28] =?UTF-8?q?=E2=9E=95=20worklog=20comments=20made=20t?=
 =?UTF-8?q?heir=20way=20to=20Jira=20worklogs?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.core/JwlCoreProcess.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/jwl.core/JwlCoreProcess.cs b/jwl.core/JwlCoreProcess.cs
index e7389a9..abe4050 100644
--- a/jwl.core/JwlCoreProcess.cs
+++ b/jwl.core/JwlCoreProcess.cs
@@ -156,7 +156,7 @@ private async Task FillJiraWithWorklogs(InputWorkLog[] inputWorklogs, WorkLog[]
         Task[] fillJiraWithWorklogsTasks = worklogsForDeletion
             .Select(worklog => _jiraClient.DeleteWorklog(worklog.IssueId, worklog.Id))
             .Concat(inputWorklogs
-                .Select(worklog => _jiraClient.AddWorklog(worklog.IssueKey.ToString(), DateOnly.FromDateTime(worklog.Date), (int)worklog.TimeSpent.TotalSeconds, worklog.WorkLogActivity, string.Empty))
+                .Select(worklog => _jiraClient.AddWorklog(worklog.IssueKey.ToString(), DateOnly.FromDateTime(worklog.Date), (int)worklog.TimeSpent.TotalSeconds, worklog.WorkLogActivity, worklog.WorkLogComment))
             )
             .ToArray();
 

From c2a895ead696d020e49803b5934533944a8d24df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 10:53:22 +0100
Subject: [PATCH 07/28] =?UTF-8?q?=F0=9F=94=A7=20launch=20settings=20git-ig?=
 =?UTF-8?q?nored?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index 9877504..e12cefa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -349,3 +349,4 @@ MigrationBackup/
 
 # Ionide (cross platform F# VS Code tools) working folder
 .ionide/
+/jwl.console/Properties/launchSettings.json

From 7b5285d1b2918fe974f2f5eb2cfd8bebb1c1212d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 10:53:49 +0100
Subject: [PATCH 08/28] =?UTF-8?q?=F0=9F=94=A7=20varible/parameter=20naming?=
 =?UTF-8?q?=20fixed?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/IJiraServerApi.cs          |  6 +++---
 jwl.jira/JiraWithICTimePluginApi.cs |  6 +++---
 jwl.jira/JiraWithTempoPluginApi.cs  | 12 ++++++------
 jwl.jira/VanillaJiraServerApi.cs    |  8 ++++----
 4 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/jwl.jira/IJiraServerApi.cs b/jwl.jira/IJiraServerApi.cs
index 36981be..07c9203 100644
--- a/jwl.jira/IJiraServerApi.cs
+++ b/jwl.jira/IJiraServerApi.cs
@@ -13,11 +13,11 @@ public interface IJiraServerApi
 
     Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<string>? issueKeys);
 
-    Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment);
+    Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment);
 
-    Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? tempoWorklogType, string? comment, bool includeNonWorkingDays = false);
+    Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false);
 
     Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false);
 
-    Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment);
+    Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment);
 }
diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
index 2bacd8c..171c8b2 100644
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -32,12 +32,12 @@ public Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<
         throw new NotImplementedException();
     }
 
-    public Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment)
+    public Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
     {
         throw new NotImplementedException();
     }
 
-    public Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? tempoWorklogType, string? comment, bool includeNonWorkingDays = false)
+    public Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
     {
         throw new NotImplementedException();
     }
@@ -47,7 +47,7 @@ public Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false
         throw new NotImplementedException();
     }
 
-    public Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment)
+    public Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
     {
         throw new NotImplementedException();
     }
diff --git a/jwl.jira/JiraWithTempoPluginApi.cs b/jwl.jira/JiraWithTempoPluginApi.cs
index 5a529c3..ddd4975 100644
--- a/jwl.jira/JiraWithTempoPluginApi.cs
+++ b/jwl.jira/JiraWithTempoPluginApi.cs
@@ -83,9 +83,9 @@ public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnume
         return result;
     }
 
-    public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment)
+    public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
     {
-        await AddWorklogPeriod(issueKey, day, day, timeSpentSeconds, worklogType, comment);
+        await AddWorklogPeriod(issueKey, day, day, timeSpentSeconds, activity, comment);
     }
 
     public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? tempoWorklogType, string? comment, bool includeNonWorkingDays = false)
@@ -129,12 +129,12 @@ public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers =
         await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
     }
 
-    public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment)
+    public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
     {
-        await UpdateWorklogPeriod(issueKey, worklogId, day, day, timeSpentSeconds, comment, worklogType);
+        await UpdateWorklogPeriod(issueKey, worklogId, day, day, timeSpentSeconds, comment, activity);
     }
 
-    private async Task UpdateWorklogPeriod(string issueKey, long worklogId, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? comment, string? tempoWorklogType, bool includeNonWorkingDays = false)
+    private async Task UpdateWorklogPeriod(string issueKey, long worklogId, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? comment, string? activity, bool includeNonWorkingDays = false)
     {
         UriBuilder uriBuilder = new UriBuilder()
         {
@@ -157,7 +157,7 @@ private async Task UpdateWorklogPeriod(string issueKey, long worklogId, DateOnly
                     Key = WorklogTypeAttributeKey,
                     Name = @"Worklog Type",
                     Type = api.rest.common.TempoWorklogAttributeTypeIdentifier.StaticList,
-                    Value = tempoWorklogType
+                    Value = activity
                 }
             }
         };
diff --git a/jwl.jira/VanillaJiraServerApi.cs b/jwl.jira/VanillaJiraServerApi.cs
index 45036cf..ff53d60 100644
--- a/jwl.jira/VanillaJiraServerApi.cs
+++ b/jwl.jira/VanillaJiraServerApi.cs
@@ -77,7 +77,7 @@ public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnume
         return result;
     }
 
-    public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment)
+    public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
     {
         UriBuilder uriBuilder = new UriBuilder()
         {
@@ -97,7 +97,7 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
         await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
     }
 
-    public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? worklogType, string? comment, bool includeNonWorkingDays = false)
+    public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
     {
         DateOnly[] daysInPeriod = Enumerable.Range(0, dayFrom.NumberOfDaysTo(dayTo))
             .Select(i => dayFrom.AddDays(i))
@@ -110,7 +110,7 @@ public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly d
         int timeSpentSecondsPerSingleDay = timeSpentSeconds / daysInPeriod.Length;
 
         Task[] addWorklogTasks = daysInPeriod
-            .Select(day => AddWorklog(issueKey, day, timeSpentSecondsPerSingleDay, worklogType, comment))
+            .Select(day => AddWorklog(issueKey, day, timeSpentSecondsPerSingleDay, activity, comment))
             .ToArray();
 
         await Task.WhenAll(addWorklogTasks);
@@ -130,7 +130,7 @@ public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers =
         await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
     }
 
-    public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? worklogType, string? comment)
+    public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
     {
         UriBuilder uriBuilder = new UriBuilder()
         {

From e114ae195d23aa743c51cb33f0ac9b4521c4c771 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 10:58:32 +0100
Subject: [PATCH 09/28] =?UTF-8?q?=F0=9F=A7=B9=20http=20client=20made=20rea?=
 =?UTF-8?q?donly?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/JiraWithICTimePluginApi.cs | 5 +++--
 jwl.jira/VanillaJiraServerApi.cs    | 4 ++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
index 171c8b2..c637aa7 100644
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -5,12 +5,13 @@
 public class JiraWithICTimePluginApi
     : IJiraServerApi
 {
-    public HttpClient HttpClient { get; }
     public string UserName { get; }
 
+    private readonly HttpClient _httpClient { get; }
+
     public JiraWithICTimePluginApi(HttpClient httpClient, string userName)
     {
-        HttpClient = httpClient;
+        _httpClient = httpClient;
         UserName = userName;
         _vanillaJiraApi = new VanillaJiraServerApi(httpClient, userName);
     }
diff --git a/jwl.jira/VanillaJiraServerApi.cs b/jwl.jira/VanillaJiraServerApi.cs
index ff53d60..d4d1531 100644
--- a/jwl.jira/VanillaJiraServerApi.cs
+++ b/jwl.jira/VanillaJiraServerApi.cs
@@ -9,10 +9,10 @@ namespace jwl.jira;
 public class VanillaJiraServerApi
     : IJiraServerApi
 {
-    private readonly HttpClient _httpClient;
-
     public string UserName { get; }
 
+    private readonly HttpClient _httpClient;
+
     public VanillaJiraServerApi(HttpClient httpClient, string userName)
     {
         _httpClient = httpClient;

From 33e194a5a257462ae721a125b7f50b6479d23fde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 11:00:16 +0100
Subject: [PATCH 10/28] =?UTF-8?q?=F0=9F=A7=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/JiraWithICTimePluginApi.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
index c637aa7..0b6995f 100644
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -7,7 +7,7 @@ public class JiraWithICTimePluginApi
 {
     public string UserName { get; }
 
-    private readonly HttpClient _httpClient { get; }
+    private readonly HttpClient _httpClient;
 
     public JiraWithICTimePluginApi(HttpClient httpClient, string userName)
     {

From b8ffa5175602add1a7fe5ce578ae7283fdfff71f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 11:02:28 +0100
Subject: [PATCH 11/28] =?UTF-8?q?=F0=9F=A7=B9=20IJiraServerApi=20interface?=
 =?UTF-8?q?=20renamed,=20since=20it's=20a=20client=20interface,=20not=20a?=
 =?UTF-8?q?=20server=20interface=20=F0=9F=99=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.core/JwlCoreProcess.cs                                 | 2 +-
 jwl.jira/{IJiraServerApi.cs => IJiraClient.cs}             | 2 +-
 jwl.jira/JiraWithICTimePluginApi.cs                        | 6 +++---
 jwl.jira/JiraWithTempoPluginApi.cs                         | 6 +++---
 jwl.jira/ServerApiFactory.cs                               | 4 ++--
 jwl.jira/{VanillaJiraServerApi.cs => VanillaJiraClient.cs} | 6 +++---
 6 files changed, 13 insertions(+), 13 deletions(-)
 rename jwl.jira/{IJiraServerApi.cs => IJiraClient.cs} (93%)
 rename jwl.jira/{VanillaJiraServerApi.cs => VanillaJiraClient.cs} (95%)

diff --git a/jwl.core/JwlCoreProcess.cs b/jwl.core/JwlCoreProcess.cs
index abe4050..28937f4 100644
--- a/jwl.core/JwlCoreProcess.cs
+++ b/jwl.core/JwlCoreProcess.cs
@@ -21,7 +21,7 @@ public class JwlCoreProcess : IDisposable
     private AppConfig _config;
     private HttpClientHandler _httpClientHandler;
     private HttpClient _httpClient;
-    private IJiraServerApi _jiraClient;
+    private IJiraClient _jiraClient;
 
     private jwl.jira.api.rest.common.JiraUserInfo? _userInfo;
     private Dictionary<string, WorkLogType> availableWorklogTypes = new ();
diff --git a/jwl.jira/IJiraServerApi.cs b/jwl.jira/IJiraClient.cs
similarity index 93%
rename from jwl.jira/IJiraServerApi.cs
rename to jwl.jira/IJiraClient.cs
index 07c9203..bbef248 100644
--- a/jwl.jira/IJiraServerApi.cs
+++ b/jwl.jira/IJiraClient.cs
@@ -5,7 +5,7 @@
 using System.Text;
 using System.Threading.Tasks;
 
-public interface IJiraServerApi
+public interface IJiraClient
 {
     Task<api.rest.common.JiraUserInfo> GetUserInfo();
 
diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
index 0b6995f..b858ad3 100644
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -3,7 +3,7 @@
 
 // https://interconcept.atlassian.net/wiki/spaces/ICTIME/pages/31686672/API
 public class JiraWithICTimePluginApi
-    : IJiraServerApi
+    : IJiraClient
 {
     public string UserName { get; }
 
@@ -13,10 +13,10 @@ public JiraWithICTimePluginApi(HttpClient httpClient, string userName)
     {
         _httpClient = httpClient;
         UserName = userName;
-        _vanillaJiraApi = new VanillaJiraServerApi(httpClient, userName);
+        _vanillaJiraApi = new VanillaJiraClient(httpClient, userName);
     }
 
-    private readonly VanillaJiraServerApi _vanillaJiraApi;
+    private readonly VanillaJiraClient _vanillaJiraApi;
 
     public async Task<JiraUserInfo> GetUserInfo()
     {
diff --git a/jwl.jira/JiraWithTempoPluginApi.cs b/jwl.jira/JiraWithTempoPluginApi.cs
index ddd4975..39b0128 100644
--- a/jwl.jira/JiraWithTempoPluginApi.cs
+++ b/jwl.jira/JiraWithTempoPluginApi.cs
@@ -5,19 +5,19 @@ namespace jwl.jira;
 
 // https://www.tempo.io/server-api-documentation/timesheets
 public class JiraWithTempoPluginApi
-    : IJiraServerApi
+    : IJiraClient
 {
     private const string WorklogTypeAttributeKey = @"_WorklogType_";
 
     private readonly HttpClient _httpClient;
-    private readonly VanillaJiraServerApi _vanillaJiraServerApi;
+    private readonly VanillaJiraClient _vanillaJiraServerApi;
 
     public string UserName { get; }
 
     public JiraWithTempoPluginApi(HttpClient httpClient, string userName)
     {
         _httpClient = httpClient;
-        _vanillaJiraServerApi = new VanillaJiraServerApi(httpClient, userName);
+        _vanillaJiraServerApi = new VanillaJiraClient(httpClient, userName);
 
         UserName = userName;
     }
diff --git a/jwl.jira/ServerApiFactory.cs b/jwl.jira/ServerApiFactory.cs
index 913bb81..508be99 100644
--- a/jwl.jira/ServerApiFactory.cs
+++ b/jwl.jira/ServerApiFactory.cs
@@ -2,11 +2,11 @@
 
 public static class ServerApiFactory
 {
-    public static IJiraServerApi CreateApi(HttpClient httpClient, string userName, JiraServerFlavour serverClass)
+    public static IJiraClient CreateApi(HttpClient httpClient, string userName, JiraServerFlavour serverClass)
     {
         return serverClass switch
         {
-            JiraServerFlavour.Vanilla => new VanillaJiraServerApi(httpClient, userName),
+            JiraServerFlavour.Vanilla => new VanillaJiraClient(httpClient, userName),
             JiraServerFlavour.TempoTimeSheets => new JiraWithTempoPluginApi(httpClient, userName),
             _ => throw new NotImplementedException($"Jira server class {nameof(serverClass)} not yet implemented")
         };
diff --git a/jwl.jira/VanillaJiraServerApi.cs b/jwl.jira/VanillaJiraClient.cs
similarity index 95%
rename from jwl.jira/VanillaJiraServerApi.cs
rename to jwl.jira/VanillaJiraClient.cs
index d4d1531..9b7706d 100644
--- a/jwl.jira/VanillaJiraServerApi.cs
+++ b/jwl.jira/VanillaJiraClient.cs
@@ -6,14 +6,14 @@ namespace jwl.jira;
 using System.Xml.Linq;
 using jwl.infra;
 
-public class VanillaJiraServerApi
-    : IJiraServerApi
+public class VanillaJiraClient
+    : IJiraClient
 {
     public string UserName { get; }
 
     private readonly HttpClient _httpClient;
 
-    public VanillaJiraServerApi(HttpClient httpClient, string userName)
+    public VanillaJiraClient(HttpClient httpClient, string userName)
     {
         _httpClient = httpClient;
         UserName = userName;

From 84708fe4029f142d8d5fbf78ca1ad9feffa516ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 17:07:38 +0100
Subject: [PATCH 12/28] =?UTF-8?q?=F0=9F=9B=A0=20ICTime=20plugin=20API=20mo?=
 =?UTF-8?q?ck'd=20via=20vanilla=20Jira=20API=20for=20the=20time=20being?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.console/CLI.cs                   |  2 +-
 jwl.jira/JiraServerFlavour.cs        |  2 +-
 jwl.jira/JiraWithICTimePluginApi.cs  | 55 -----------------------
 jwl.jira/JiraWithICTimePluginMock.cs | 67 ++++++++++++++++++++++++++++
 jwl.jira/ServerApiFactory.cs         |  1 +
 5 files changed, 70 insertions(+), 57 deletions(-)
 delete mode 100644 jwl.jira/JiraWithICTimePluginApi.cs
 create mode 100644 jwl.jira/JiraWithICTimePluginMock.cs

diff --git a/jwl.console/CLI.cs b/jwl.console/CLI.cs
index 52db571..e29da74 100644
--- a/jwl.console/CLI.cs
+++ b/jwl.console/CLI.cs
@@ -22,7 +22,7 @@ public class FillCLI
 
     [Option("server-flavour", HelpText = "Jira server flavour (whether vanilla or with some timesheet plugins)"
         + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.ServerFlavour)}"
-        + $"\nAvailable values: {nameof(jira.JiraServerFlavour.Vanilla)}, {nameof(jira.JiraServerFlavour.TempoTimeSheets)}, {nameof(jira.JiraServerFlavour.ICTime)}")]
+        + $"\nAvailable values: {nameof(jira.JiraServerFlavour.Vanilla)}, {nameof(jira.JiraServerFlavour.TempoTimeSheets)}, {nameof(jira.JiraServerFlavour.ICTimeMockViaJira)}")]
     public string? ServerClass { get; set; }
 
     [Option("no-proxy", HelpText = "Turn off proxying the HTTP(S) connections to Jira server"
diff --git a/jwl.jira/JiraServerFlavour.cs b/jwl.jira/JiraServerFlavour.cs
index 10efe2b..544d4a2 100644
--- a/jwl.jira/JiraServerFlavour.cs
+++ b/jwl.jira/JiraServerFlavour.cs
@@ -4,5 +4,5 @@ public enum JiraServerFlavour
 {
     Vanilla = 0,
     TempoTimeSheets = 1,
-    ICTime = 2
+    ICTimeMockViaJira = 2
 }
diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
deleted file mode 100644
index b858ad3..0000000
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-namespace jwl.jira;
-using jwl.jira.api.rest.common;
-
-// https://interconcept.atlassian.net/wiki/spaces/ICTIME/pages/31686672/API
-public class JiraWithICTimePluginApi
-    : IJiraClient
-{
-    public string UserName { get; }
-
-    private readonly HttpClient _httpClient;
-
-    public JiraWithICTimePluginApi(HttpClient httpClient, string userName)
-    {
-        _httpClient = httpClient;
-        UserName = userName;
-        _vanillaJiraApi = new VanillaJiraClient(httpClient, userName);
-    }
-
-    private readonly VanillaJiraClient _vanillaJiraApi;
-
-    public async Task<JiraUserInfo> GetUserInfo()
-    {
-        return await _vanillaJiraApi.GetUserInfo();
-    }
-
-    public Task<WorkLogType[]> GetWorklogTypes()
-    {
-        throw new NotImplementedException();
-    }
-
-    public Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<string>? issueKeys)
-    {
-        throw new NotImplementedException();
-    }
-
-    public Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
-    {
-        throw new NotImplementedException();
-    }
-
-    public Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
-    {
-        throw new NotImplementedException();
-    }
-
-    public Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false)
-    {
-        throw new NotImplementedException();
-    }
-
-    public Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
-    {
-        throw new NotImplementedException();
-    }
-}
diff --git a/jwl.jira/JiraWithICTimePluginMock.cs b/jwl.jira/JiraWithICTimePluginMock.cs
new file mode 100644
index 0000000..e797103
--- /dev/null
+++ b/jwl.jira/JiraWithICTimePluginMock.cs
@@ -0,0 +1,67 @@
+namespace jwl.jira;
+using jwl.jira.api.rest.common;
+using System.Text;
+
+// https://interconcept.atlassian.net/wiki/spaces/ICTIME/pages/31686672/API
+// https://interconcept.atlassian.net/wiki/spaces/ICBIZ/pages/34701333/REST+Services
+public class JiraWithICTimePluginMock
+    : IJiraClient
+{
+    public string UserName { get; }
+
+    private readonly HttpClient _httpClient;
+
+    public JiraWithICTimePluginMock(HttpClient httpClient, string userName)
+    {
+        _httpClient = httpClient;
+        UserName = userName;
+        _vanillaJiraApi = new VanillaJiraClient(httpClient, userName);
+    }
+
+    private readonly VanillaJiraClient _vanillaJiraApi;
+
+    public async Task<JiraUserInfo> GetUserInfo()
+    {
+        return await _vanillaJiraApi.GetUserInfo();
+    }
+
+    public async Task<WorkLogType[]> GetWorklogTypes()
+    {
+        return await _vanillaJiraApi.GetWorklogTypes();
+    }
+
+    public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<string>? issueKeys)
+    {
+        return await _vanillaJiraApi.GetIssueWorklogs(from, to, issueKeys);
+    }
+
+    public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+    {
+        StringBuilder commentBuilder = new StringBuilder();
+        if (!string.IsNullOrWhiteSpace(activity))
+            commentBuilder.Append($"({activity}){Environment.NewLine}");
+        commentBuilder.Append(comment);
+
+        await _vanillaJiraApi.AddWorklog(issueKey, day, timeSpentSeconds, null, commentBuilder.ToString());
+    }
+
+    public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
+    {
+        StringBuilder commentBuilder = new StringBuilder();
+        if (!string.IsNullOrWhiteSpace(activity))
+            commentBuilder.Append($"({activity}){Environment.NewLine}");
+        commentBuilder.Append(comment);
+
+        await _vanillaJiraApi.AddWorklogPeriod(issueKey, dayFrom, dayTo, timeSpentSeconds, null, commentBuilder.ToString(), includeNonWorkingDays);
+    }
+
+    public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false)
+    {
+        await _vanillaJiraApi.DeleteWorklog(issueId, worklogId, notifyUsers);
+    }
+
+    public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+    {
+        await _vanillaJiraApi.UpdateWorklog(issueKey, worklogId, day, timeSpentSeconds, activity, comment);
+    }
+}
diff --git a/jwl.jira/ServerApiFactory.cs b/jwl.jira/ServerApiFactory.cs
index 508be99..2b62719 100644
--- a/jwl.jira/ServerApiFactory.cs
+++ b/jwl.jira/ServerApiFactory.cs
@@ -8,6 +8,7 @@ public static IJiraClient CreateApi(HttpClient httpClient, string userName, Jira
         {
             JiraServerFlavour.Vanilla => new VanillaJiraClient(httpClient, userName),
             JiraServerFlavour.TempoTimeSheets => new JiraWithTempoPluginApi(httpClient, userName),
+            JiraServerFlavour.ICTimeMockViaJira => new JiraWithICTimePluginMock(httpClient, userName),
             _ => throw new NotImplementedException($"Jira server class {nameof(serverClass)} not yet implemented")
         };
     }

From d90ad42a878a10abf1a4b32480e72c152d9b056e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 21:57:32 +0100
Subject: [PATCH 13/28] =?UTF-8?q?=F0=9F=9B=A0=20AppConfig.OverrideWith()?=
 =?UTF-8?q?=20now=20works=20more-or-less-OK=20with=20ActivityMap=20diction?=
 =?UTF-8?q?ary?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.core/AppConfig.cs | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/jwl.core/AppConfig.cs b/jwl.core/AppConfig.cs
index 171a8ad..52760f1 100644
--- a/jwl.core/AppConfig.cs
+++ b/jwl.core/AppConfig.cs
@@ -8,7 +8,7 @@ public class AppConfig
     public jwl.core.UserConfig? User { get; init; }
     public jwl.inputs.CsvFormatConfig? CsvOptions { get; init; }
 
-    private static MapperConfiguration overridingMapperConfiguration = new MapperConfiguration(cfg =>
+    private static Lazy<MapperConfiguration> overridingMapperConfiguration = new (() => new MapperConfiguration(cfg =>
     {
         cfg.CreateMap<AppConfig, AppConfig>()
             .ForAllMembers(m => m.Condition((src, dest, member) => member != null));
@@ -17,15 +17,19 @@ public class AppConfig
         cfg.CreateMap<inputs.CsvFormatConfig, inputs.CsvFormatConfig>()
             .ForAllMembers(m => m.Condition((src, dest, member) => member != null));
         cfg.CreateMap<core.UserConfig, core.UserConfig>()
-            .ForAllMembers(m => m.Condition((src, dest, member) => member != null));
-    });
-    private static IMapper overridingMapper = overridingMapperConfiguration.CreateMapper();
+            .ForAllMembers(m => m.Condition((src, dest, member) => member != null));
+
+        cfg.AddGlobalIgnore(nameof(AppConfig.JiraServer.ActivityMap));
+    }));
+
+    private static Lazy<IMapper> overridingMapper = new (() => overridingMapperConfiguration.Value.CreateMapper());
 
     public AppConfig OverrideWith(AppConfig? other)
     {
         if (other == null)
-            return this;
-
-        return overridingMapper.Map<AppConfig, AppConfig>(other, this);
+            return this;
+
+        AppConfig result = overridingMapper.Value.Map<AppConfig, AppConfig>(other, this);
+        return result;
     }
 }

From 643bbbb4046d9bfe185f1b8ff5b11165d3237744 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 21:58:10 +0100
Subject: [PATCH 14/28] =?UTF-8?q?=F0=9F=94=A7=20fix:=20default=20server=20?=
 =?UTF-8?q?flavour=20is=20now=20Vanilla?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.core/AppConfigFactory.cs | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/jwl.core/AppConfigFactory.cs b/jwl.core/AppConfigFactory.cs
index 4059176..99b0947 100644
--- a/jwl.core/AppConfigFactory.cs
+++ b/jwl.core/AppConfigFactory.cs
@@ -16,6 +16,8 @@ public static AppConfig CreateWithDefaults()
         {
             JiraServer = new ServerConfig()
             {
+                ServerFlavour = nameof(JiraServerFlavour.Vanilla),
+                ActivityMap = null,
                 BaseUrl = @"http://jira.my-domain.xyz",
                 MaxConnectionsPerServer = DefaultMaxConnectionsPerServer,
                 SkipSslCertificateCheck = false,

From 4fb00cf02f65163bbe787da4aa615cccc0901787 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 22:00:35 +0100
Subject: [PATCH 15/28] =?UTF-8?q?=E2=9E=95=20ICTime=20plugin=20client=20AP?=
 =?UTF-8?q?I=20(...=20still=20not=20working,=20though)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/JiraServerFlavour.cs                 |   2 +-
 ...uginMock.cs => JiraWithICTimePluginApi.cs} | 151 ++++++++++--------
 jwl.jira/ServerApiFactory.cs                  |   2 +-
 .../request/ICTimeAddWorklogByIssueKey.cs     |   6 +
 4 files changed, 96 insertions(+), 65 deletions(-)
 rename jwl.jira/{JiraWithICTimePluginMock.cs => JiraWithICTimePluginApi.cs} (51%)
 create mode 100644 jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs

diff --git a/jwl.jira/JiraServerFlavour.cs b/jwl.jira/JiraServerFlavour.cs
index 544d4a2..10efe2b 100644
--- a/jwl.jira/JiraServerFlavour.cs
+++ b/jwl.jira/JiraServerFlavour.cs
@@ -4,5 +4,5 @@ public enum JiraServerFlavour
 {
     Vanilla = 0,
     TempoTimeSheets = 1,
-    ICTimeMockViaJira = 2
+    ICTime = 2
 }
diff --git a/jwl.jira/JiraWithICTimePluginMock.cs b/jwl.jira/JiraWithICTimePluginApi.cs
similarity index 51%
rename from jwl.jira/JiraWithICTimePluginMock.cs
rename to jwl.jira/JiraWithICTimePluginApi.cs
index e797103..b5a833a 100644
--- a/jwl.jira/JiraWithICTimePluginMock.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -1,67 +1,92 @@
-namespace jwl.jira;
-using jwl.jira.api.rest.common;
+namespace jwl.jira;
+
+using jwl.infra;
+using jwl.jira.api.rest.common;
+using System.Diagnostics;
+using System.Net.Http.Json;
+using System.Numerics;
 using System.Text;
 
 // https://interconcept.atlassian.net/wiki/spaces/ICTIME/pages/31686672/API
 // https://interconcept.atlassian.net/wiki/spaces/ICBIZ/pages/34701333/REST+Services
-public class JiraWithICTimePluginMock
-    : IJiraClient
-{
-    public string UserName { get; }
-
-    private readonly HttpClient _httpClient;
-
-    public JiraWithICTimePluginMock(HttpClient httpClient, string userName)
-    {
-        _httpClient = httpClient;
-        UserName = userName;
-        _vanillaJiraApi = new VanillaJiraClient(httpClient, userName);
-    }
-
-    private readonly VanillaJiraClient _vanillaJiraApi;
-
-    public async Task<JiraUserInfo> GetUserInfo()
-    {
-        return await _vanillaJiraApi.GetUserInfo();
-    }
-
-    public async Task<WorkLogType[]> GetWorklogTypes()
-    {
-        return await _vanillaJiraApi.GetWorklogTypes();
-    }
-
-    public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<string>? issueKeys)
-    {
-        return await _vanillaJiraApi.GetIssueWorklogs(from, to, issueKeys);
-    }
-
-    public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
-    {
-        StringBuilder commentBuilder = new StringBuilder();
-        if (!string.IsNullOrWhiteSpace(activity))
-            commentBuilder.Append($"({activity}){Environment.NewLine}");
-        commentBuilder.Append(comment);
-
-        await _vanillaJiraApi.AddWorklog(issueKey, day, timeSpentSeconds, null, commentBuilder.ToString());
-    }
-
-    public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
-    {
-        StringBuilder commentBuilder = new StringBuilder();
-        if (!string.IsNullOrWhiteSpace(activity))
-            commentBuilder.Append($"({activity}){Environment.NewLine}");
-        commentBuilder.Append(comment);
-
-        await _vanillaJiraApi.AddWorklogPeriod(issueKey, dayFrom, dayTo, timeSpentSeconds, null, commentBuilder.ToString(), includeNonWorkingDays);
-    }
-
-    public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false)
-    {
-        await _vanillaJiraApi.DeleteWorklog(issueId, worklogId, notifyUsers);
-    }
-
-    public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
-    {
-        await _vanillaJiraApi.UpdateWorklog(issueKey, worklogId, day, timeSpentSeconds, activity, comment);
-    }
-}
+public class JiraWithICTimePluginApi
+    : IJiraClient
+{
+    public string UserName { get; }
+
+    private readonly HttpClient _httpClient;
+
+    public JiraWithICTimePluginApi(HttpClient httpClient, string userName)
+    {
+        _httpClient = httpClient;
+        UserName = userName;
+        _vanillaJiraApi = new VanillaJiraClient(httpClient, userName);
+    }
+
+    private readonly VanillaJiraClient _vanillaJiraApi;
+
+    public async Task<JiraUserInfo> GetUserInfo()
+    {
+        return await _vanillaJiraApi.GetUserInfo();
+    }
+
+    public async Task<WorkLogType[]> GetWorklogTypes()
+    {
+        return await _vanillaJiraApi.GetWorklogTypes();
+    }
+
+    public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<string>? issueKeys)
+    {
+        return await _vanillaJiraApi.GetIssueWorklogs(from, to, issueKeys);
+    }
+
+    public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+    {
+        UriBuilder uriBuilder = new UriBuilder()
+        {
+            Path = new UriPathBuilder(@"rest/api/2/issue")
+                .Add(issueKey)
+                .Add(@"worklog")
+        };
+        var request = new api.rest.request.ICTimeAddWorklogByIssueKey(
+            Started: day
+                .ToDateTime(TimeOnly.MinValue)
+                .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
+                .Replace(":", string.Empty)
+                .Replace(';', ':'),
+            TimeSpentSeconds: timeSpentSeconds,
+            Activity: string.IsNullOrEmpty(activity) ? null : int.Parse(activity),
+            Comment: comment
+        );
+        await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+    }
+
+    public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
+    {
+        DateOnly[] daysInPeriod = Enumerable.Range(0, dayFrom.NumberOfDaysTo(dayTo))
+            .Select(i => dayFrom.AddDays(i))
+            .Where(day => includeNonWorkingDays || day.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday)
+            .ToArray();
+
+        if (!daysInPeriod.Any())
+            return;
+
+        int timeSpentSecondsPerSingleDay = timeSpentSeconds / daysInPeriod.Length;
+
+        Task[] addWorklogTasks = daysInPeriod
+            .Select(day => AddWorklog(issueKey, day, timeSpentSecondsPerSingleDay, activity, comment))
+            .ToArray();
+
+        await Task.WhenAll(addWorklogTasks);
+    }
+
+    public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false)
+    {
+        await _vanillaJiraApi.DeleteWorklog(issueId, worklogId, notifyUsers);
+    }
+
+    public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
+    {
+        await _vanillaJiraApi.UpdateWorklog(issueKey, worklogId, day, timeSpentSeconds, activity, comment);
+    }
+}
diff --git a/jwl.jira/ServerApiFactory.cs b/jwl.jira/ServerApiFactory.cs
index 2b62719..9cae3a4 100644
--- a/jwl.jira/ServerApiFactory.cs
+++ b/jwl.jira/ServerApiFactory.cs
@@ -8,7 +8,7 @@ public static IJiraClient CreateApi(HttpClient httpClient, string userName, Jira
         {
             JiraServerFlavour.Vanilla => new VanillaJiraClient(httpClient, userName),
             JiraServerFlavour.TempoTimeSheets => new JiraWithTempoPluginApi(httpClient, userName),
-            JiraServerFlavour.ICTimeMockViaJira => new JiraWithICTimePluginMock(httpClient, userName),
+            JiraServerFlavour.ICTime => new JiraWithICTimePluginApi(httpClient, userName),
             _ => throw new NotImplementedException($"Jira server class {nameof(serverClass)} not yet implemented")
         };
     }
diff --git a/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs b/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
new file mode 100644
index 0000000..f11261d
--- /dev/null
+++ b/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
@@ -0,0 +1,6 @@
+#pragma warning disable SA1313
+namespace jwl.jira.api.rest.request;
+
+public record ICTimeAddWorklogByIssueKey(string Started, int TimeSpentSeconds, int? Activity, string? Comment)
+{
+}

From 90bc834dfd732013026cfdcce6be826ad8aabdda Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 22:01:20 +0100
Subject: [PATCH 16/28] =?UTF-8?q?=E2=9E=95=20configurable=20activity=20map?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.console/CLI.cs       | 7 ++++---
 jwl.jira/ServerConfig.cs | 2 +-
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/jwl.console/CLI.cs b/jwl.console/CLI.cs
index e29da74..2df6d0e 100644
--- a/jwl.console/CLI.cs
+++ b/jwl.console/CLI.cs
@@ -22,8 +22,8 @@ public class FillCLI
 
     [Option("server-flavour", HelpText = "Jira server flavour (whether vanilla or with some timesheet plugins)"
         + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.ServerFlavour)}"
-        + $"\nAvailable values: {nameof(jira.JiraServerFlavour.Vanilla)}, {nameof(jira.JiraServerFlavour.TempoTimeSheets)}, {nameof(jira.JiraServerFlavour.ICTimeMockViaJira)}")]
-    public string? ServerClass { get; set; }
+        + $"\nAvailable values: {nameof(jira.JiraServerFlavour.Vanilla)}, {nameof(jira.JiraServerFlavour.TempoTimeSheets)}, {nameof(jira.JiraServerFlavour.ICTime)}")]
+    public string? ServerFlavour { get; set; }
 
     [Option("no-proxy", HelpText = "Turn off proxying the HTTP(S) connections to Jira server"
         + $"\nJSON config: $.{nameof(core.AppConfig.JiraServer)}.{nameof(jira.ServerConfig.UseProxy)} (negated!)")]
@@ -55,7 +55,8 @@ public core.AppConfig ToAppConfig()
             UseVerboseFeedback = UseVerboseFeedback,
             JiraServer = new jira.ServerConfig()
             {
-                ServerFlavour = ServerClass,
+                ServerFlavour = ServerFlavour,
+                ActivityMap = null,
                 BaseUrl = jiraServerSpecification,
                 UseProxy = !NoProxy,
                 MaxConnectionsPerServer = MaxConnectionsPerServer,
diff --git a/jwl.jira/ServerConfig.cs b/jwl.jira/ServerConfig.cs
index d7d6440..3c5ae3e 100644
--- a/jwl.jira/ServerConfig.cs
+++ b/jwl.jira/ServerConfig.cs
@@ -1,10 +1,10 @@
 namespace jwl.jira;
-using System.ComponentModel;
 
 public class ServerConfig
 {
     public string? ServerFlavour { get; init; }
     public JiraServerFlavour ServerFlavourId => ServerApiFactory.DecodeServerClass(ServerFlavour) ?? JiraServerFlavour.Vanilla;
+    public Dictionary<string, string>? ActivityMap { get; init; }
     public string? BaseUrl { get; init; }
     public bool? UseProxy { get; init; }
     public int? MaxConnectionsPerServer { get; init; }

From 63f06bfeec0868090adb7c423a024cd015603503 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 22:01:57 +0100
Subject: [PATCH 17/28] =?UTF-8?q?=E2=9E=95=20activity=20now=20gets=20refle?=
 =?UTF-8?q?cted=20in=20vanilla=20Jira=20worklog=20comment?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/VanillaJiraClient.cs | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/jwl.jira/VanillaJiraClient.cs b/jwl.jira/VanillaJiraClient.cs
index 9b7706d..b20a127 100644
--- a/jwl.jira/VanillaJiraClient.cs
+++ b/jwl.jira/VanillaJiraClient.cs
@@ -3,6 +3,7 @@ namespace jwl.jira;
 using System.Collections.Generic;
 using System.Net.Http;
 using System.Net.Http.Json;
+using System.Text;
 using System.Xml.Linq;
 using jwl.infra;
 
@@ -69,7 +70,7 @@ public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnume
                 Created: wl.Created.Value,
                 Started: wl.Started.Value,
                 TimeSpentSeconds: wl.TimeSpentSeconds,
-                WorkLogType: null,
+                Activity: null,
                 Comment: wl.Comment
             ))
             .ToArray();
@@ -85,6 +86,12 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
                 .Add(issueKey)
                 .Add(@"worklog")
         };
+
+        StringBuilder commentBuilder = new StringBuilder();
+        if (activity != null)
+            commentBuilder.Append($"({activity}){Environment.NewLine}");
+        commentBuilder.Append(comment);
+
         var request = new api.rest.request.JiraAddWorklogByIssueKey(
             Started: day
                 .ToDateTime(TimeOnly.MinValue)
@@ -92,7 +99,7 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
                 .Replace(":", string.Empty)
                 .Replace(';', ':'),
             TimeSpentSeconds: timeSpentSeconds,
-            Comment: comment
+            Comment: commentBuilder.ToString()
         );
         await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
     }

From 90039772aaaa1ad674b8fed741f73233d283e101 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 22:02:40 +0100
Subject: [PATCH 18/28] =?UTF-8?q?=F0=9F=94=A7=20worklogtype=20renamed=20to?=
 =?UTF-8?q?=20activity?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/JiraWithTempoPluginApi.cs | 2 +-
 jwl.jira/WorkLog.cs                | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/jwl.jira/JiraWithTempoPluginApi.cs b/jwl.jira/JiraWithTempoPluginApi.cs
index 39b0128..44f5049 100644
--- a/jwl.jira/JiraWithTempoPluginApi.cs
+++ b/jwl.jira/JiraWithTempoPluginApi.cs
@@ -75,7 +75,7 @@ public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnume
                 Created: wl.Created?.Value ?? DateTime.MinValue,
                 Started: wl.Started?.Value ?? DateTime.MinValue,
                 TimeSpentSeconds: wl.TimeSpentSeconds ?? -1,
-                WorkLogType: wl.Attributes?[WorklogTypeAttributeKey].Value,
+                Activity: wl.Attributes?[WorklogTypeAttributeKey].Value,
                 Comment: wl.Comment ?? string.Empty
             ))
             .ToArray();
diff --git a/jwl.jira/WorkLog.cs b/jwl.jira/WorkLog.cs
index a5232bc..e2a8c3b 100644
--- a/jwl.jira/WorkLog.cs
+++ b/jwl.jira/WorkLog.cs
@@ -1,6 +1,6 @@
 #pragma warning disable SA1313
 namespace jwl.jira;
 
-public record WorkLog(long Id, long IssueId, string? AuthorName, string? AuthorKey, DateTime Created, DateTime Started, int TimeSpentSeconds, string? WorkLogType, string Comment)
+public record WorkLog(long Id, long IssueId, string? AuthorName, string? AuthorKey, DateTime Created, DateTime Started, int TimeSpentSeconds, string? Activity, string Comment)
 {
 }

From c8e9b90611df3b9ae0bd461ff2c240839c45e56a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 22:03:30 +0100
Subject: [PATCH 19/28] =?UTF-8?q?=E2=9E=95=20worklog=20activity=20now=20ge?=
 =?UTF-8?q?ts=20retrieved=20from=20activity=20map,=20if=20configured,=20ot?=
 =?UTF-8?q?herwise=20the=20original=20activity=20string=20gets=20used?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.core/JwlCoreProcess.cs | 510 +++++++++++++++++++------------------
 jwl.core/jwl.core.csproj   |   4 +-
 2 files changed, 262 insertions(+), 252 deletions(-)

diff --git a/jwl.core/JwlCoreProcess.cs b/jwl.core/JwlCoreProcess.cs
index 28937f4..8f4b3f4 100644
--- a/jwl.core/JwlCoreProcess.cs
+++ b/jwl.core/JwlCoreProcess.cs
@@ -1,253 +1,261 @@
-namespace jwl.core;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
-using jwl.infra;
-using jwl.inputs;
-using jwl.jira;
-using jwl.jira.api.rest.response;
-using Microsoft.Extensions.Configuration;
-using NoP77svk.Linq;
-
-public class JwlCoreProcess : IDisposable
-{
-    public const int TotalProcessSteps = 7;
-
-    public ICoreProcessFeedback? Feedback { get; init; }
-    public ICoreProcessInteraction _interaction { get; }
-
-    private bool _isDisposed;
-
-    private AppConfig _config;
-    private HttpClientHandler _httpClientHandler;
-    private HttpClient _httpClient;
-    private IJiraClient _jiraClient;
-
-    private jwl.jira.api.rest.common.JiraUserInfo? _userInfo;
-    private Dictionary<string, WorkLogType> availableWorklogTypes = new ();
-
-    public JwlCoreProcess(AppConfig config, ICoreProcessInteraction interaction)
-    {
-        _config = config;
-        _interaction = interaction;
-
-        _httpClientHandler = new HttpClientHandler()
-        {
-            UseProxy = _config.JiraServer?.UseProxy ?? false,
-            UseDefaultCredentials = false,
-            MaxConnectionsPerServer = _config.JiraServer?.MaxConnectionsPerServer ?? AppConfigFactory.DefaultMaxConnectionsPerServer
-        };
-
-        if (_config.JiraServer?.SkipSslCertificateCheck ?? false)
-            _httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
-
-        _httpClient = new HttpClient(_httpClientHandler)
-        {
-            BaseAddress = new Uri(_config.JiraServer?.BaseUrl ?? string.Empty)
+namespace jwl.core;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using jwl.infra;
+using jwl.inputs;
+using jwl.jira;
+using NoP77svk.Linq;
+
+public class JwlCoreProcess : IDisposable
+{
+    public const int TotalProcessSteps = 7;
+
+    public ICoreProcessFeedback? Feedback { get; init; }
+    public ICoreProcessInteraction _interaction { get; }
+
+    private bool _isDisposed;
+
+    private AppConfig _config;
+    private HttpClientHandler _httpClientHandler;
+    private HttpClient _httpClient;
+    private IJiraClient _jiraClient;
+
+    private jwl.jira.api.rest.common.JiraUserInfo? _userInfo;
+
+    public JwlCoreProcess(AppConfig config, ICoreProcessInteraction interaction)
+    {
+        _config = config;
+        _interaction = interaction;
+
+        _httpClientHandler = new HttpClientHandler()
+        {
+            UseProxy = _config.JiraServer?.UseProxy ?? false,
+            UseDefaultCredentials = false,
+            MaxConnectionsPerServer = _config.JiraServer?.MaxConnectionsPerServer ?? AppConfigFactory.DefaultMaxConnectionsPerServer
+        };
+
+        if (_config.JiraServer?.SkipSslCertificateCheck ?? false)
+            _httpClientHandler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true;
+
+        _httpClient = new HttpClient(_httpClientHandler)
+        {
+            BaseAddress = new Uri(_config.JiraServer?.BaseUrl ?? string.Empty)
+        };
+
+        string userName = _config.User?.Name ?? throw new ArgumentNullException($"{nameof(_config)}.{nameof(_config.User)}.{nameof(_config.User.Name)})");
+        _jiraClient = ServerApiFactory.CreateApi(_httpClient, userName, _config.JiraServer?.ServerFlavourId ?? JiraServerFlavour.Vanilla);
+
+        /* 2do!...
+        _jiraClient.WsClient.HttpRequestPostprocess = req =>
+        {
+            // 2do! optional logging of request bodies
+        };
+
+        _jiraClient.WsClient.HttpResponsePostprocess = resp =>
+        {
+            // 2do! optional logging of response bodies
+        };
+        */
+    }
+
+    public async Task PreProcess()
+    {
+        Feedback?.OverallProcessStart();
+
+        string? jiraUserName = _config.User?.Name;
+        string? jiraUserPassword = _config.User?.Password;
+
+        if (string.IsNullOrEmpty(jiraUserName) || string.IsNullOrEmpty(jiraUserPassword))
+        {
+            if (_interaction != null)
+                (jiraUserName, jiraUserPassword) = _interaction.AskForCredentials(jiraUserName);
+        }
+
+        if (string.IsNullOrEmpty(jiraUserName) || string.IsNullOrEmpty(jiraUserPassword))
+            throw new ArgumentNullException($"Jira credentials not supplied");
+
+        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(@"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(jiraUserName + ":" + jiraUserPassword)));
+
+        Feedback?.PreloadUserInfoStart(jiraUserName);
+        _userInfo = await _jiraClient.GetUserInfo();
+        Feedback?.PreloadUserInfoEnd();
+    }
+
+    public async Task Process(IEnumerable<string> inputFiles)
+    {
+        string[] inputFilesFetched = inputFiles.ToArray();
+
+        if (inputFilesFetched.Length > 0)
+        {
+            Feedback?.ReadCsvInputStart();
+            InputWorkLog[] inputWorklogs = await ReadInputFiles(inputFiles);
+            Feedback?.ReadCsvInputEnd();
+
+            if (inputWorklogs.Length > 0)
+            {
+                Feedback?.RetrieveWorklogsForDeletionStart();
+                WorkLog[] worklogsForDeletion = await RetrieveWorklogsForDeletion(inputWorklogs);
+                Feedback?.RetrieveWorklogsForDeletionEnd();
+
+                Feedback?.FillJiraWithWorklogsStart();
+                await FillJiraWithWorklogs(inputWorklogs, worklogsForDeletion);
+                Feedback?.FillJiraWithWorklogsEnd();
+            }
+            else
+            {
+                Feedback?.NoWorklogsToFill();
+            }
+
+            Feedback?.OverallProcessEnd();
+        }
+        else
+        {
+            Feedback?.NoFilesOnInput();
+        }
+    }
+
+    public void Dispose()
+    {
+        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+        Dispose(disposing: true);
+        GC.SuppressFinalize(this);
+    }
+
+    protected virtual void Dispose(bool disposing)
+    {
+        if (!_isDisposed)
+        {
+            if (disposing)
+            {
+            }
+
+            // note: free unmanaged resources (unmanaged objects) and override finalizer
+            // note: set large fields to null
+            _httpClient?.Dispose();
+            _httpClientHandler?.Dispose();
+
+            _isDisposed = true;
+        }
+    }
+
+    private async Task FillJiraWithWorklogs(InputWorkLog[] inputWorklogs, WorkLog[] worklogsForDeletion)
+    {
+        Feedback?.FillJiraWithWorklogsSetTarget(inputWorklogs.Length, worklogsForDeletion.Length);
+
+        if (_userInfo == null || _userInfo.Key == null)
+            throw new ArgumentNullException(@"Unresolved Jira key for the logged-on user");
+
+        Task[] fillJiraWithWorklogsTasks = worklogsForDeletion
+            .Select(worklog => _jiraClient.DeleteWorklog(worklog.IssueId, worklog.Id))
+            .Concat(inputWorklogs
+                .LeftOuterJoin(
+                    innerTable: _config.JiraServer?.ActivityMap ?? new Dictionary<string, string>(),
+                    outerKeySelector: wl => wl.WorkLogActivity.ToLower(),
+                    innerKeySelector: am => am.Key.ToLower(),
+                    resultSelector: (wl, am) => new ValueTuple<InputWorkLog, string?>(wl, am.Value)
+                )
+                .Select(x => _jiraClient.AddWorklog(
+                    issueKey: x.Item1.IssueKey.ToString(),
+                    day: DateOnly.FromDateTime(x.Item1.Date),
+                    timeSpentSeconds: (int)x.Item1.TimeSpent.TotalSeconds,
+                    activity: !(_config.JiraServer?.ActivityMap?.Any() ?? false) ? x.Item1.WorkLogActivity : x.Item2,
+                    comment: x.Item1.WorkLogComment
+                ))
+            )
+            .ToArray();
+
+        MultiTaskProgress progress = new MultiTaskProgress(fillJiraWithWorklogsTasks.Length);
+
+        await MultiTask.WhenAll(
+            tasks: fillJiraWithWorklogsTasks,
+            progressFeedback: (_, t) => Feedback?.FillJiraWithWorklogsProcess(progress.AddTaskStatus(t?.Status))
+        );
+    }
+
+    private async Task<InputWorkLog[]> ReadInputFiles(IEnumerable<string> fileNames)
+    {
+        Task<InputWorkLog[]>[] readerTasks = fileNames
+            .Select(fileName => ReadInputFile(fileName))
+            .ToArray();
+
+        Feedback?.ReadCsvInputSetTarget(readerTasks.Length);
+
+        MultiTaskProgress progress = new MultiTaskProgress(readerTasks.Length);
+
+        if (readerTasks.Any())
+        {
+            await MultiTask.WhenAll(
+                tasks: readerTasks,
+                progressFeedback: (_, t) => Feedback?.ReadCsvInputProcess(progress.AddTaskStatus(t?.Status))
+            );
+        }
+
+        InputWorkLog[] result = readerTasks
+            .SelectMany(response => response.Result)
+            .ToArray();
+
+        return result;
+    }
+
+    private async Task<InputWorkLog[]> ReadInputFile(string fileName)
+    {
+        WorklogReaderAggregatedConfig readerConfig = new WorklogReaderAggregatedConfig()
+        {
+            CsvFormatConfig = _config.CsvOptions
         };
 
-        string userName = _config.User?.Name ?? throw new ArgumentNullException($"{nameof(_config)}.{nameof(_config.User)}.{nameof(_config.User.Name)})");
-        _jiraClient = ServerApiFactory.CreateApi(_httpClient, userName, _config.JiraServer?.ServerFlavourId ?? JiraServerFlavour.Vanilla);
-
-        /* 2do!...
-        _jiraClient.WsClient.HttpRequestPostprocess = req =>
-        {
-            // 2do! optional logging of request bodies
-        };
-
-        _jiraClient.WsClient.HttpResponsePostprocess = resp =>
-        {
-            // 2do! optional logging of response bodies
-        };
-        */
-    }
-
-    public async Task PreProcess()
-    {
-        Feedback?.OverallProcessStart();
-
-        string? jiraUserName = _config.User?.Name;
-        string? jiraUserPassword = _config.User?.Password;
-
-        if (string.IsNullOrEmpty(jiraUserName) || string.IsNullOrEmpty(jiraUserPassword))
-        {
-            if (_interaction != null)
-                (jiraUserName, jiraUserPassword) = _interaction.AskForCredentials(jiraUserName);
-        }
-
-        if (string.IsNullOrEmpty(jiraUserName) || string.IsNullOrEmpty(jiraUserPassword))
-            throw new ArgumentNullException($"Jira credentials not supplied");
-
-        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(@"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(jiraUserName + ":" + jiraUserPassword)));
-
-        Feedback?.PreloadAvailableWorklogTypesStart();
-        availableWorklogTypes = (await _jiraClient.GetWorklogTypes()).ToDictionary(wlt => wlt.Value);
-        Feedback?.PreloadAvailableWorklogTypesEnd();
-
-        Feedback?.PreloadUserInfoStart(jiraUserName);
-        _userInfo = await _jiraClient.GetUserInfo();
-        Feedback?.PreloadUserInfoEnd();
-    }
-
-    public async Task Process(IEnumerable<string> inputFiles)
-    {
-        string[] inputFilesFetched = inputFiles.ToArray();
-
-        if (inputFilesFetched.Length > 0)
-        {
-            Feedback?.ReadCsvInputStart();
-            InputWorkLog[] inputWorklogs = await ReadInputFiles(inputFiles);
-            Feedback?.ReadCsvInputEnd();
-
-            if (inputWorklogs.Length > 0)
-            {
-                Feedback?.RetrieveWorklogsForDeletionStart();
-                WorkLog[] worklogsForDeletion = await RetrieveWorklogsForDeletion(inputWorklogs);
-                Feedback?.RetrieveWorklogsForDeletionEnd();
-
-                Feedback?.FillJiraWithWorklogsStart();
-                await FillJiraWithWorklogs(inputWorklogs, worklogsForDeletion);
-                Feedback?.FillJiraWithWorklogsEnd();
-            }
-            else
-            {
-                Feedback?.NoWorklogsToFill();
-            }
-
-            Feedback?.OverallProcessEnd();
-        }
-        else
-        {
-            Feedback?.NoFilesOnInput();
-        }
-    }
-
-    public void Dispose()
-    {
-        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
-        Dispose(disposing: true);
-        GC.SuppressFinalize(this);
-    }
-
-    protected virtual void Dispose(bool disposing)
-    {
-        if (!_isDisposed)
-        {
-            if (disposing)
-            {
-            }
-
-            // note: free unmanaged resources (unmanaged objects) and override finalizer
-            // note: set large fields to null
-            _httpClient?.Dispose();
-            _httpClientHandler?.Dispose();
-
-            _isDisposed = true;
-        }
-    }
-
-    private async Task FillJiraWithWorklogs(InputWorkLog[] inputWorklogs, WorkLog[] worklogsForDeletion)
-    {
-        Feedback?.FillJiraWithWorklogsSetTarget(inputWorklogs.Length, worklogsForDeletion.Length);
-
-        if (_userInfo == null || _userInfo.Key == null)
-            throw new ArgumentNullException(@"Unresolved Jira key for the logged-on user");
-
-        Task[] fillJiraWithWorklogsTasks = worklogsForDeletion
-            .Select(worklog => _jiraClient.DeleteWorklog(worklog.IssueId, worklog.Id))
-            .Concat(inputWorklogs
-                .Select(worklog => _jiraClient.AddWorklog(worklog.IssueKey.ToString(), DateOnly.FromDateTime(worklog.Date), (int)worklog.TimeSpent.TotalSeconds, worklog.WorkLogActivity, worklog.WorkLogComment))
-            )
-            .ToArray();
-
-        MultiTaskProgress progress = new MultiTaskProgress(fillJiraWithWorklogsTasks.Length);
-
-        await MultiTask.WhenAll(
-            tasks: fillJiraWithWorklogsTasks,
-            progressFeedback: (_, t) => Feedback?.FillJiraWithWorklogsProcess(progress.AddTaskStatus(t?.Status))
-        );
-    }
-
-    private async Task<InputWorkLog[]> ReadInputFiles(IEnumerable<string> fileNames)
-    {
-        Task<InputWorkLog[]>[] readerTasks = fileNames
-            .Select(fileName => ReadInputFile(fileName))
-            .ToArray();
-
-        Feedback?.ReadCsvInputSetTarget(readerTasks.Length);
-
-        MultiTaskProgress progress = new MultiTaskProgress(readerTasks.Length);
-
-        if (readerTasks.Any())
-        {
-            await MultiTask.WhenAll(
-                tasks: readerTasks,
-                progressFeedback: (_, t) => Feedback?.ReadCsvInputProcess(progress.AddTaskStatus(t?.Status))
-            );
-        }
-
-        InputWorkLog[] result = readerTasks
-            .SelectMany(response => response.Result)
-            .ToArray();
-
-        return result;
-    }
-
-    private async Task<InputWorkLog[]> ReadInputFile(string fileName)
-    {
-        WorklogReaderAggregatedConfig readerConfig = new WorklogReaderAggregatedConfig()
-        {
-            CsvFormatConfig = _config.CsvOptions
-        };
-
-        using IWorklogReader worklogReader = WorklogReaderFactory.GetReaderFromFilePath(fileName, readerConfig);
-
-        Task<InputWorkLog[]> response = Task.Factory.StartNew(() =>
-        {
-            return worklogReader
-                .Read(row =>
-                {
-                    if (!availableWorklogTypes.ContainsKey(row.WorkLogActivity))
-                        throw new InvalidDataException($"Worklog type {row.WorkLogActivity} not found on server");
-                })
-                .ToArray();
-        });
-
-        return await response;
-    }
-
-    private async Task<WorkLog[]> RetrieveWorklogsForDeletion(InputWorkLog[] inputWorklogs)
-    {
-        WorkLog[] result;
-
-        if (_userInfo == null)
-            throw new ArgumentNullException(@"User info not preloaded from Jira server");
-        if (string.IsNullOrEmpty(_userInfo.Key))
-            throw new ArgumentNullException(@"Empty user key preloaded from Jira server");
-
-        DateTime[] inputWorklogDays = inputWorklogs
-            .Select(worklog => worklog.Date)
-            .OrderBy(worklogDate => worklogDate)
-            .ToArray();
-
-        if (!inputWorklogDays.Any())
-        {
-            result = Array.Empty<WorkLog>();
-        }
-        else
-        {
-            DateOnly minInputWorklogDay = DateOnly.FromDateTime(inputWorklogDays.First().Date);
-            DateOnly maxInputWorklogDay = DateOnly.FromDateTime(inputWorklogDays.Last().Date);
-
-            string[] inputIssueKeys = inputWorklogs
-                .Select(worklog => worklog.IssueKey.ToString())
-                .Distinct()
-                .OrderByDescending(x => x)
-                .ToArray();
-
-            result = await _jiraClient.GetIssueWorklogs(minInputWorklogDay, maxInputWorklogDay, inputIssueKeys);
-        }
-
-        return result;
-    }
-}
+        using IWorklogReader worklogReader = WorklogReaderFactory.GetReaderFromFilePath(fileName, readerConfig);
+
+        Task<InputWorkLog[]> response = Task.Factory.StartNew(() =>
+        {
+            return worklogReader
+                .Read(row =>
+                {
+                    if (row.WorkLogActivity is not null)
+                    {
+                        if (!_config.JiraServer?.ActivityMap?.ContainsKey(row.WorkLogActivity) ?? false)
+                            throw new InvalidDataException($"Worklog type {row.WorkLogActivity} not found in activity map");
+                    }
+                })
+                .ToArray();
+        });
+
+        return await response;
+    }
+
+    private async Task<WorkLog[]> RetrieveWorklogsForDeletion(InputWorkLog[] inputWorklogs)
+    {
+        WorkLog[] result;
+
+        if (_userInfo == null)
+            throw new ArgumentNullException(@"User info not preloaded from Jira server");
+        if (string.IsNullOrEmpty(_userInfo.Key))
+            throw new ArgumentNullException(@"Empty user key preloaded from Jira server");
+
+        DateTime[] inputWorklogDays = inputWorklogs
+            .Select(worklog => worklog.Date)
+            .OrderBy(worklogDate => worklogDate)
+            .ToArray();
+
+        if (!inputWorklogDays.Any())
+        {
+            result = Array.Empty<WorkLog>();
+        }
+        else
+        {
+            DateOnly minInputWorklogDay = DateOnly.FromDateTime(inputWorklogDays.First().Date);
+            DateOnly maxInputWorklogDay = DateOnly.FromDateTime(inputWorklogDays.Last().Date);
+
+            string[] inputIssueKeys = inputWorklogs
+                .Select(worklog => worklog.IssueKey.ToString())
+                .Distinct()
+                .OrderByDescending(x => x)
+                .ToArray();
+
+            result = await _jiraClient.GetIssueWorklogs(minInputWorklogDay, maxInputWorklogDay, inputIssueKeys);
+        }
+
+        return result;
+    }
+}
diff --git a/jwl.core/jwl.core.csproj b/jwl.core/jwl.core.csproj
index 00bae86..009a83d 100644
--- a/jwl.core/jwl.core.csproj
+++ b/jwl.core/jwl.core.csproj
@@ -28,7 +28,9 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
-    <PackageReference Include="NoP77svk.Linq" Version="2023.3.1" />
+    <PackageReference Include="NoP77svk.Commons" Version="2023.10.1" />
+    <PackageReference Include="NoP77svk.Linq" Version="2023.10.1" />
+    <PackageReference Include="NoP77svk.Linq.OuterJoins" Version="2023.10.1" />
     <PackageReference Include="StyleCop.Analyzers" Version="*">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

From 04adb9434675680164c95f060d120eae761a2ee3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 22:58:38 +0100
Subject: [PATCH 20/28] =?UTF-8?q?=E2=9E=95=20Jira=20REST=20error=20message?=
 =?UTF-8?q?s=20handling?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/IJiraClient.cs                       |  2 +-
 jwl.jira/JiraWithICTimePluginApi.cs           |  9 +++++---
 jwl.jira/JiraWithTempoPluginApi.cs            | 13 +++++++----
 jwl.jira/VanillaJiraClient.cs                 | 23 +++++++++++++++----
 .../request/ICTimeAddWorklogByIssueKey.cs     |  3 +++
 .../api/rest/response/JiraRestResponse.cs     |  6 +++++
 6 files changed, 44 insertions(+), 12 deletions(-)
 create mode 100644 jwl.jira/api/rest/response/JiraRestResponse.cs

diff --git a/jwl.jira/IJiraClient.cs b/jwl.jira/IJiraClient.cs
index bbef248..9fa6adb 100644
--- a/jwl.jira/IJiraClient.cs
+++ b/jwl.jira/IJiraClient.cs
@@ -9,7 +9,7 @@ public interface IJiraClient
 {
     Task<api.rest.common.JiraUserInfo> GetUserInfo();
 
-    Task<WorkLogType[]> GetWorklogTypes();
+    Task<WorkLogType[]> GetAvailableActivities();
 
     Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<string>? issueKeys);
 
diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
index b5a833a..3f23f65 100644
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -2,6 +2,7 @@
 
 using jwl.infra;
 using jwl.jira.api.rest.common;
+using jwl.jira.api.rest.response;
 using System.Diagnostics;
 using System.Net.Http.Json;
 using System.Numerics;
@@ -30,9 +31,9 @@ public async Task<JiraUserInfo> GetUserInfo()
         return await _vanillaJiraApi.GetUserInfo();
     }
 
-    public async Task<WorkLogType[]> GetWorklogTypes()
+    public async Task<WorkLogType[]> GetAvailableActivities()
     {
-        return await _vanillaJiraApi.GetWorklogTypes();
+        return await _vanillaJiraApi.GetAvailableActivities();
     }
 
     public async Task<WorkLog[]> GetIssueWorklogs(DateOnly from, DateOnly to, IEnumerable<string>? issueKeys)
@@ -58,7 +59,9 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
             Activity: string.IsNullOrEmpty(activity) ? null : int.Parse(activity),
             Comment: comment
         );
-        await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+
+        HttpResponseMessage response = await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+        await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
     }
 
     public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
diff --git a/jwl.jira/JiraWithTempoPluginApi.cs b/jwl.jira/JiraWithTempoPluginApi.cs
index 44f5049..b85fb0a 100644
--- a/jwl.jira/JiraWithTempoPluginApi.cs
+++ b/jwl.jira/JiraWithTempoPluginApi.cs
@@ -32,7 +32,7 @@ public async Task<JiraUserInfo> GetUserInfo()
         return await _httpClient.GetAsJsonAsync<api.rest.response.TempoWorklogAttributeDefinition[]>(@"rest/tempo-core/1/work-attribute");
     }
 
-    public async Task<WorkLogType[]> GetWorklogTypes()
+    public async Task<WorkLogType[]> GetAvailableActivities()
     {
         api.rest.response.TempoWorklogAttributeDefinition[] attrEnumDefs = await GetWorklogAttributeDefinitions();
 
@@ -116,7 +116,8 @@ public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly d
             }
         };
 
-        await _httpClient.PostAsJsonAsync(@"rest/tempo-timesheets/4/worklogs", request);
+        HttpResponseMessage response = await _httpClient.PostAsJsonAsync(@"rest/tempo-timesheets/4/worklogs", request);
+        await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
     }
 
     public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers = false)
@@ -126,7 +127,9 @@ public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers =
             Path = new UriPathBuilder(@"rest/tempo-timesheets/4/worklogs")
                 .Add(worklogId.ToString())
         };
-        await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
+
+        HttpResponseMessage response = await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
+        await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
     }
 
     public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
@@ -161,6 +164,8 @@ private async Task UpdateWorklogPeriod(string issueKey, long worklogId, DateOnly
                 }
             }
         };
-        await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+
+        HttpResponseMessage response = await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+        await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
     }
 }
diff --git a/jwl.jira/VanillaJiraClient.cs b/jwl.jira/VanillaJiraClient.cs
index b20a127..93a3507 100644
--- a/jwl.jira/VanillaJiraClient.cs
+++ b/jwl.jira/VanillaJiraClient.cs
@@ -6,6 +6,7 @@ namespace jwl.jira;
 using System.Text;
 using System.Xml.Linq;
 using jwl.infra;
+using jwl.jira.api.rest.response;
 
 public class VanillaJiraClient
     : IJiraClient
@@ -20,6 +21,14 @@ public VanillaJiraClient(HttpClient httpClient, string userName)
         UserName = userName;
     }
 
+    public static async Task CheckHttpResponseForErrorMessages(HttpResponseMessage responseMessage)
+    {
+        using Stream responseContentStream = await responseMessage.Content.ReadAsStreamAsync();
+        JiraRestResponse responseContent = await HttpClientJsonExt.DeserializeJsonStreamAsync<JiraRestResponse>(responseContentStream);
+        if (responseContent?.ErrorMessages is not null && responseContent.ErrorMessages.Any())
+            throw new InvalidOperationException(string.Join(Environment.NewLine, responseContent.ErrorMessages));
+    }
+
     public async Task<api.rest.common.JiraUserInfo> GetUserInfo()
     {
         UriBuilder uriBuilder = new UriBuilder()
@@ -32,7 +41,7 @@ public VanillaJiraClient(HttpClient httpClient, string userName)
     }
 
     #pragma warning disable CS1998
-    public async Task<WorkLogType[]> GetWorklogTypes()
+    public async Task<WorkLogType[]> GetAvailableActivities()
     {
         return Array.Empty<WorkLogType>();
     }
@@ -101,7 +110,9 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
             TimeSpentSeconds: timeSpentSeconds,
             Comment: commentBuilder.ToString()
         );
-        await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+
+        HttpResponseMessage response = await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+        await CheckHttpResponseForErrorMessages(response);
     }
 
     public async Task AddWorklogPeriod(string issueKey, DateOnly dayFrom, DateOnly dayTo, int timeSpentSeconds, string? activity, string? comment, bool includeNonWorkingDays = false)
@@ -134,7 +145,9 @@ public async Task DeleteWorklog(long issueId, long worklogId, bool notifyUsers =
             Query = new UriQueryBuilder()
                 .Add(@"notifyUsers", notifyUsers.ToString().ToLower())
         };
-        await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
+
+        HttpResponseMessage response = await _httpClient.DeleteAsync(uriBuilder.Uri.PathAndQuery);
+        await CheckHttpResponseForErrorMessages(response);
     }
 
     public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, int timeSpentSeconds, string? activity, string? comment)
@@ -155,6 +168,8 @@ public async Task UpdateWorklog(string issueKey, long worklogId, DateOnly day, i
             TimeSpentSeconds: timeSpentSeconds,
             Comment: comment
         );
-        await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+
+        HttpResponseMessage response = await _httpClient.PutAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
+        await CheckHttpResponseForErrorMessages(response);
     }
 }
diff --git a/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs b/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
index f11261d..9295613 100644
--- a/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
+++ b/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
@@ -3,4 +3,7 @@ namespace jwl.jira.api.rest.request;
 
 public record ICTimeAddWorklogByIssueKey(string Started, int TimeSpentSeconds, int? Activity, string? Comment)
 {
+    public string LogWorkOption { get; init; } = "summary";
+    public string AdjustEstimate { get; init; } = "auto";
+    public bool ActivateLogwork { get; init; } = true;
 }
diff --git a/jwl.jira/api/rest/response/JiraRestResponse.cs b/jwl.jira/api/rest/response/JiraRestResponse.cs
new file mode 100644
index 0000000..6a2bbe2
--- /dev/null
+++ b/jwl.jira/api/rest/response/JiraRestResponse.cs
@@ -0,0 +1,6 @@
+namespace jwl.jira.api.rest.response;
+
+internal class JiraRestResponse
+{
+    public string[]? ErrorMessages { get; init; }
+}
\ No newline at end of file

From 6265b1d18b929c8c89b07179f56953211854ed1f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Mon, 29 Jan 2024 23:21:15 +0100
Subject: [PATCH 21/28] =?UTF-8?q?=E2=9E=96=20NameValueCollection.AsEnumera?=
 =?UTF-8?q?ble()=20removed=20as=20reundant=20to=20.Cast()?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.infra/NameValueCollectionExt.cs | 13 -------------
 jwl.infra/UriQueryBuilder.cs        |  2 +-
 2 files changed, 1 insertion(+), 14 deletions(-)
 delete mode 100644 jwl.infra/NameValueCollectionExt.cs

diff --git a/jwl.infra/NameValueCollectionExt.cs b/jwl.infra/NameValueCollectionExt.cs
deleted file mode 100644
index b16e37a..0000000
--- a/jwl.infra/NameValueCollectionExt.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace jwl.infra;
-using System.Collections.Specialized;
-
-public static class NameValueCollectionExt
-{
-    public static IEnumerable<KeyValuePair<string?, string?>> AsEnumerable(this NameValueCollection self)
-    {
-        foreach (string? key in self.AllKeys)
-        {
-            yield return new KeyValuePair<string?, string?>(key, self[key]);
-        }
-    }
-}
\ No newline at end of file
diff --git a/jwl.infra/UriQueryBuilder.cs b/jwl.infra/UriQueryBuilder.cs
index 4824967..8e5389e 100644
--- a/jwl.infra/UriQueryBuilder.cs
+++ b/jwl.infra/UriQueryBuilder.cs
@@ -11,7 +11,7 @@ public UriQueryBuilder()
 
     public UriQueryBuilder(string uriQuery)
         : base(HttpUtility.ParseQueryString(uriQuery)
-            .AsEnumerable()
+            .Cast<KeyValuePair<string?, string?>>()
             .ToList())
     {
     }

From bb04cf811cdfa312e404d876523b95ab1458d7bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Tue, 30 Jan 2024 10:07:47 +0100
Subject: [PATCH 22/28] =?UTF-8?q?=E2=9E=95=20ICTime=20"add=20worklog"=20re?=
 =?UTF-8?q?quest=20enhanced=20with=20"custom=20field"=20(...=20yet=20there?=
 =?UTF-8?q?'s=20more=20work=20to=20do=20here)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/JiraWithICTimePluginApi.cs           |  2 ++
 .../request/ICTimeAddWorklogByIssueKey.cs     | 30 ++++++++++++++++---
 2 files changed, 28 insertions(+), 4 deletions(-)

diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
index 3f23f65..39394db 100644
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -50,6 +50,7 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
                 .Add(@"worklog")
         };
         var request = new api.rest.request.ICTimeAddWorklogByIssueKey(
+            IssueKey: issueKey,
             Started: day
                 .ToDateTime(TimeOnly.MinValue)
                 .ToString(@"yyyy-MM-dd""T""hh"";""mm"";""ss.fffzzzz")
@@ -59,6 +60,7 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
             Activity: string.IsNullOrEmpty(activity) ? null : int.Parse(activity),
             Comment: comment
         );
+        // 2do! annotate the request.CustomFieldNNN with JSON field name based on ICTime server metadata (retrieved previously)
 
         HttpResponseMessage response = await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
         await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);
diff --git a/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs b/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
index 9295613..40824c3 100644
--- a/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
+++ b/jwl.jira/api/rest/request/ICTimeAddWorklogByIssueKey.cs
@@ -1,9 +1,31 @@
 #pragma warning disable SA1313
 namespace jwl.jira.api.rest.request;
+using System.Text;
+using System.Text.Json.Serialization;
 
-public record ICTimeAddWorklogByIssueKey(string Started, int TimeSpentSeconds, int? Activity, string? Comment)
+public record ICTimeAddWorklogByIssueKey(string IssueKey, string Started, int TimeSpentSeconds, int? Activity, string? Comment)
 {
-    public string LogWorkOption { get; init; } = "summary";
-    public string AdjustEstimate { get; init; } = "auto";
-    public bool ActivateLogwork { get; init; } = true;
+    [JsonIgnore]
+    public string NoCharge { get; } = "off";
+    [JsonIgnore]
+    public string? NoChargeInfo { get; } = null;
+    [JsonIgnore]
+    public string LogWorkOption { get; } = "summary";
+    [JsonIgnore]
+    public string AdjustEstimate { get; } = "auto";
+    [JsonIgnore]
+    public bool ActivateLogwork { get; } = true;
+
+    public string CustomFieldNNN => new StringBuilder()
+        // noChargeInfo==why this should not be charged||timeSpentCorrected==5h||newEstimate==5w||startDate==Sep 29, 2014 9:34 AM||startTime==5:05 PM||endTime==5:07 PM||
+        .Append($"adjustEstimate=={AdjustEstimate}||")
+        .Append($"nocharge=={NoCharge}||")
+        .Append(!string.IsNullOrEmpty(NoChargeInfo) ? $"noChargeInfo=={NoCharge}||" : string.Empty)
+        .Append(Activity is not null ? $"activity=={Activity}||" : string.Empty)
+        .Append(Comment is not null ? $"comment=={Comment}||" : string.Empty)
+        .Append($"issueKey=={IssueKey}||")
+        .Append($"logWorkOption=={LogWorkOption}||")
+        .Append($"timeLogged=={TimeSpan.FromSeconds(TimeSpentSeconds).ToString()}||") // 2do!
+        .Append($"activateLogwork=={ActivateLogwork.ToString().ToLower()}||")
+        .ToString();
 }

From 0dbf674e6dfa830df910b17e8f2567a14faacacc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?= <phr@whitestein.com>
Date: Tue, 30 Jan 2024 10:08:38 +0100
Subject: [PATCH 23/28] =?UTF-8?q?=F0=9F=A7=B9=20style=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 jwl.jira/JiraWithICTimePluginApi.cs | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/jwl.jira/JiraWithICTimePluginApi.cs b/jwl.jira/JiraWithICTimePluginApi.cs
index 39394db..e55ccd1 100644
--- a/jwl.jira/JiraWithICTimePluginApi.cs
+++ b/jwl.jira/JiraWithICTimePluginApi.cs
@@ -49,6 +49,8 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
                 .Add(issueKey)
                 .Add(@"worklog")
         };
+
+        // 2do! annotate the request.CustomFieldNNN with JSON field name based on ICTime server metadata (retrieved previously)
         var request = new api.rest.request.ICTimeAddWorklogByIssueKey(
             IssueKey: issueKey,
             Started: day
@@ -60,7 +62,6 @@ public async Task AddWorklog(string issueKey, DateOnly day, int timeSpentSeconds
             Activity: string.IsNullOrEmpty(activity) ? null : int.Parse(activity),
             Comment: comment
         );
-        // 2do! annotate the request.CustomFieldNNN with JSON field name based on ICTime server metadata (retrieved previously)
 
         HttpResponseMessage response = await _httpClient.PostAsJsonAsync(uriBuilder.Uri.PathAndQuery, request);
         await VanillaJiraClient.CheckHttpResponseForErrorMessages(response);

From 6ded1a41d220d12f6c6f1168fc36e841866d7ca1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?=
 <16204822+nop77svk@users.noreply.github.com>
Date: Wed, 31 Jan 2024 12:58:05 +0000
Subject: [PATCH 24/28] =?UTF-8?q?=F0=9F=9B=A0=20fix=20multitask=20handling?=
 =?UTF-8?q?=20of=20progress=20feedback=20(#7)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 🛠 MultiTask now handles exceptions and generates AggregateException

* 🔧 fix: WorklogCsvReader was not invoking row postprocess

* ➕ fix: handling of error response containing JSON error messages

* 🛠 fix: MultiTask now handles slave task exceptions correctly

* 🧹 formatting

* 🔧 multitask progress state enum moved inside the MultiTask class

* 🛠 multitask progress fixed

* 🛠 multitask progress stats decoupled from the multitask

* 🔧 multitask now handles internal state changes more generically
---
 .../ScrollingConsoleProcessFeedback.cs        |   8 +-
 jwl.core/ICoreProcessFeedback.cs              |   6 +-
 jwl.core/JwlCoreProcess.cs                    |  24 ++--
 .../MultiTaskStats.cs                         |  11 +-
 jwl.infra/MultiTask.cs                        | 128 ++++++++++++------
 jwl.infra/MultiTaskProgressState.cs           |  11 --
 jwl.inputs/WorklogCsvReader.cs                |   2 +
 jwl.jira/VanillaJiraClient.cs                 |  11 +-
 8 files changed, 124 insertions(+), 77 deletions(-)
 rename jwl.infra/MultiTaskProgress.cs => jwl.core/MultiTaskStats.cs (87%)
 delete mode 100644 jwl.infra/MultiTaskProgressState.cs

diff --git a/jwl.console/ScrollingConsoleProcessFeedback.cs b/jwl.console/ScrollingConsoleProcessFeedback.cs
index a2d5c5b..82c0c69 100644
--- a/jwl.console/ScrollingConsoleProcessFeedback.cs
+++ b/jwl.console/ScrollingConsoleProcessFeedback.cs
@@ -36,7 +36,7 @@ public void FillJiraWithWorklogsSetTarget(int numberOfWorklogsToInsert, int numb
         Console.Error.Write($"\rFilling Jira with worklogs (+{_numberOfWorklogsToInsert}/-{_numberOfWorklogsToDelete})...");
     }
 
-    public void FillJiraWithWorklogsProcess(MultiTaskProgress progress)
+    public void FillJiraWithWorklogsProcess(MultiTaskStats progress)
     {
         Console.Error.Write($"\rFilling Jira with worklogs (+{_numberOfWorklogsToInsert}/-{_numberOfWorklogsToDelete})... {ProgressPercentageAsString(progress)}");
     }
@@ -104,7 +104,7 @@ public void ReadCsvInputSetTarget(int numberOfInputFiles)
         Console.Error.Write($"\rReading {numberOfInputFiles} input files...");
     }
 
-    public void ReadCsvInputProcess(MultiTaskProgress progress)
+    public void ReadCsvInputProcess(MultiTaskStats progress)
     {
         Console.Error.Write($"\rReading {progress.Total} input files... {ProgressPercentageAsString(progress)}");
     }
@@ -124,7 +124,7 @@ public void RetrieveWorklogsForDeletionSetTarget(int count)
         Console.Error.Write($"\rRetrieving list of worklogs ({count} Jira issues) to be deleted...");
     }
 
-    public void RetrieveWorklogsForDeletionProcess(MultiTaskProgress progress)
+    public void RetrieveWorklogsForDeletionProcess(MultiTaskStats progress)
     {
         throw new NotImplementedException(@"--- checkpoint ---");
     }
@@ -134,7 +134,7 @@ public void RetrieveWorklogsForDeletionEnd()
         Console.Error.WriteLine(" OK");
     }
 
-    protected static string ProgressPercentageAsString(MultiTaskProgress progress)
+    protected static string ProgressPercentageAsString(MultiTaskStats progress)
     {
         string result;
 
diff --git a/jwl.core/ICoreProcessFeedback.cs b/jwl.core/ICoreProcessFeedback.cs
index 34ae25c..6cd208d 100644
--- a/jwl.core/ICoreProcessFeedback.cs
+++ b/jwl.core/ICoreProcessFeedback.cs
@@ -6,7 +6,7 @@ public interface ICoreProcessFeedback
 {
     void FillJiraWithWorklogsStart();
     void FillJiraWithWorklogsSetTarget(int numberOfWorklogsToInsert, int numbeOfWorklogsToDelete);
-    void FillJiraWithWorklogsProcess(MultiTaskProgress progress);
+    void FillJiraWithWorklogsProcess(MultiTaskStats progress);
     void FillJiraWithWorklogsEnd();
     void NoExistingWorklogsToDelete();
     void NoFilesOnInput();
@@ -19,10 +19,10 @@ public interface ICoreProcessFeedback
     void PreloadUserInfoEnd();
     void ReadCsvInputStart();
     void ReadCsvInputSetTarget(int numberOfInputFiles);
-    void ReadCsvInputProcess(MultiTaskProgress progress);
+    void ReadCsvInputProcess(MultiTaskStats progress);
     void ReadCsvInputEnd();
     void RetrieveWorklogsForDeletionStart();
     void RetrieveWorklogsForDeletionSetTarget(int count);
-    void RetrieveWorklogsForDeletionProcess(MultiTaskProgress progress);
+    void RetrieveWorklogsForDeletionProcess(MultiTaskStats progress);
     void RetrieveWorklogsForDeletionEnd();
 }
diff --git a/jwl.core/JwlCoreProcess.cs b/jwl.core/JwlCoreProcess.cs
index 8f4b3f4..744f874 100644
--- a/jwl.core/JwlCoreProcess.cs
+++ b/jwl.core/JwlCoreProcess.cs
@@ -165,12 +165,13 @@ private async Task FillJiraWithWorklogs(InputWorkLog[] inputWorklogs, WorkLog[]
             )
             .ToArray();
 
-        MultiTaskProgress progress = new MultiTaskProgress(fillJiraWithWorklogsTasks.Length);
+        MultiTaskStats progress = new MultiTaskStats(fillJiraWithWorklogsTasks.Length);
+        MultiTask multiTask = new MultiTask()
+        {
+            TaskFeedback = t => Feedback?.FillJiraWithWorklogsProcess(progress.ApplyTaskStatus(t.Status))
+        };
 
-        await MultiTask.WhenAll(
-            tasks: fillJiraWithWorklogsTasks,
-            progressFeedback: (_, t) => Feedback?.FillJiraWithWorklogsProcess(progress.AddTaskStatus(t?.Status))
-        );
+        await multiTask.WhenAll(fillJiraWithWorklogsTasks);
     }
 
     private async Task<InputWorkLog[]> ReadInputFiles(IEnumerable<string> fileNames)
@@ -181,15 +182,14 @@ private async Task<InputWorkLog[]> ReadInputFiles(IEnumerable<string> fileNames)
 
         Feedback?.ReadCsvInputSetTarget(readerTasks.Length);
 
-        MultiTaskProgress progress = new MultiTaskProgress(readerTasks.Length);
+        MultiTaskStats progressStats = new MultiTaskStats(readerTasks.Length);
+        MultiTask multiTask = new MultiTask()
+        {
+            TaskFeedback = t => Feedback?.ReadCsvInputProcess(progressStats.ApplyTaskStatus(t.Status))
+        };
 
         if (readerTasks.Any())
-        {
-            await MultiTask.WhenAll(
-                tasks: readerTasks,
-                progressFeedback: (_, t) => Feedback?.ReadCsvInputProcess(progress.AddTaskStatus(t?.Status))
-            );
-        }
+            await multiTask.WhenAll(readerTasks);
 
         InputWorkLog[] result = readerTasks
             .SelectMany(response => response.Result)
diff --git a/jwl.infra/MultiTaskProgress.cs b/jwl.core/MultiTaskStats.cs
similarity index 87%
rename from jwl.infra/MultiTaskProgress.cs
rename to jwl.core/MultiTaskStats.cs
index e6b9907..fda1898 100644
--- a/jwl.infra/MultiTaskProgress.cs
+++ b/jwl.core/MultiTaskStats.cs
@@ -1,8 +1,8 @@
-namespace jwl.infra;
+namespace jwl.core;
+using jwl.infra;
 
-public class MultiTaskProgress
+public class MultiTaskStats
 {
-    public MultiTaskProgressState State { get; private set; }
     public int Total { get; private set; }
     public int Succeeded { get; private set; }
     public float SucceededPct => Total > 0 ? (float)Succeeded / Total : float.NaN;
@@ -18,10 +18,9 @@ public class MultiTaskProgress
     public int DoneSoFar => Succeeded + ErredSoFar;
     public float DoneSoFarPct => Total > 0 ? (float)DoneSoFar / Total : float.NaN;
 
-    public MultiTaskProgress(int total)
+    public MultiTaskStats(int total)
     {
         Total = total;
-        State = MultiTaskProgressState.Unknown;
         Succeeded = 0;
         Faulted = 0;
         Cancelled = 0;
@@ -30,7 +29,7 @@ public MultiTaskProgress(int total)
 
     private object _locker = new object();
 
-    public MultiTaskProgress AddTaskStatus(TaskStatus? taskStatus)
+    public MultiTaskStats ApplyTaskStatus(TaskStatus? taskStatus)
     {
         if (taskStatus == null)
             return this;
diff --git a/jwl.infra/MultiTask.cs b/jwl.infra/MultiTask.cs
index 8ac6601..2d7bb70 100644
--- a/jwl.infra/MultiTask.cs
+++ b/jwl.infra/MultiTask.cs
@@ -1,38 +1,90 @@
-namespace jwl.infra;
-
-public static class MultiTask
-{
-    public static async Task WhenAll(IEnumerable<Task> tasks, Action<MultiTaskProgressState, Task?> progressFeedback, CancellationToken? cancellationToken = null)
-    {
-        progressFeedback(MultiTaskProgressState.Starting, null);
-        HashSet<Task> tasksToExecute = tasks.ToHashSet();
-
-        try
-        {
-            while (tasksToExecute.Any())
-            {
-                cancellationToken?.ThrowIfCancellationRequested();
-
-                Task taskFinished = await Task.WhenAny(tasksToExecute);
-                progressFeedback(MultiTaskProgressState.InProgress, taskFinished);
-
-                if (tasksToExecute.Contains(taskFinished))
-                    tasksToExecute.Remove(taskFinished);
-                else
-                    throw new Exception("Task reported as finished... again!");
-            }
-        }
-        catch (TaskCanceledException)
-        {
-            progressFeedback(MultiTaskProgressState.Cancelled, null);
-            throw;
-        }
-        catch (Exception)
-        {
-            progressFeedback(MultiTaskProgressState.Error, null);
-            throw;
-        }
-
-        progressFeedback(MultiTaskProgressState.Finished, null);
-    }
-}
+namespace jwl.infra;
+
+public class MultiTask
+{
+    public enum ProgressState
+    {
+        Unknown,
+        Starting,
+        BeforeTaskWait,
+        AfterTaskWait,
+        Finished,
+        Error,
+        Cancelled
+    }
+
+    public ProgressState State { get; private set; } = ProgressState.Unknown;
+
+    public Action<MultiTask>? ProcessFeedback { get; init; }
+    public Action<Task>? TaskFeedback { get; init; }
+
+    public MultiTask()
+    {
+    }
+
+    public async Task WhenAll(IEnumerable<Task> tasks, CancellationToken? cancellationToken = null)
+    {
+        State = ProgressState.Starting;
+        ProcessFeedback?.Invoke(this);
+
+        HashSet<Task> tasksToExecute = tasks.ToHashSet();
+        List<Exception> errors = new List<Exception>();
+
+        while (tasksToExecute.Any())
+        {
+            State = ProgressState.BeforeTaskWait;
+            ProcessFeedback?.Invoke(this);
+
+            Task? taskFinished = null;
+            try
+            {
+                cancellationToken?.ThrowIfCancellationRequested();
+
+                taskFinished = await Task.WhenAny(tasksToExecute);
+
+                State = ProgressState.AfterTaskWait;
+                ProcessFeedback?.Invoke(this);
+                TaskFeedback?.Invoke(taskFinished);
+
+                if (taskFinished.Status is TaskStatus.Faulted or TaskStatus.Canceled)
+                {
+                    tasksToExecute.Remove(taskFinished);
+                    throw taskFinished.Exception ?? new Exception($"Task ended in {taskFinished.Status} status without exception details");
+                }
+                else if (taskFinished.Status == TaskStatus.RanToCompletion)
+                {
+                    if (!tasksToExecute.Remove(taskFinished))
+                        throw new InvalidOperationException("Task reported as finished... again!");
+                }
+            }
+            catch (AggregateException ex)
+            {
+                errors.AddRange(ex.InnerExceptions);
+            }
+            catch (Exception ex)
+            {
+                errors.Add(ex);
+            }
+        }
+
+        if (errors.All(ex => ex is TaskCanceledException))
+        {
+            State = ProgressState.Cancelled;
+            ProcessFeedback?.Invoke(this);
+
+            throw new TaskCanceledException($"All {errors.Count} tasks have been cancelled", new AggregateException(errors));
+        }
+        else if (errors.Any())
+        {
+            State = ProgressState.Error;
+            ProcessFeedback?.Invoke(this);
+
+            throw new AggregateException(errors);
+        }
+        else
+        {
+            State = ProgressState.Finished;
+            ProcessFeedback?.Invoke(this);
+        }
+    }
+}
diff --git a/jwl.infra/MultiTaskProgressState.cs b/jwl.infra/MultiTaskProgressState.cs
deleted file mode 100644
index 59c1495..0000000
--- a/jwl.infra/MultiTaskProgressState.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace jwl.infra;
-
-public enum MultiTaskProgressState
-{
-    Unknown,
-    Starting,
-    InProgress,
-    Finished,
-    Error,
-    Cancelled
-}
diff --git a/jwl.inputs/WorklogCsvReader.cs b/jwl.inputs/WorklogCsvReader.cs
index 443377a..297cbcc 100644
--- a/jwl.inputs/WorklogCsvReader.cs
+++ b/jwl.inputs/WorklogCsvReader.cs
@@ -73,6 +73,8 @@ public IEnumerable<InputWorkLog> Read(Action<InputWorkLog>? postProcessResult =
                     WorkLogActivity = row.WorkLogActivity,
                     WorkLogComment = row.WorkLogComment
                 };
+
+                postProcessResult?.Invoke(result);
             }
             catch (Exception e)
             {
diff --git a/jwl.jira/VanillaJiraClient.cs b/jwl.jira/VanillaJiraClient.cs
index 93a3507..1204540 100644
--- a/jwl.jira/VanillaJiraClient.cs
+++ b/jwl.jira/VanillaJiraClient.cs
@@ -4,6 +4,7 @@ namespace jwl.jira;
 using System.Net.Http;
 using System.Net.Http.Json;
 using System.Text;
+using System.Text.Json;
 using System.Xml.Linq;
 using jwl.infra;
 using jwl.jira.api.rest.response;
@@ -24,9 +25,13 @@ public VanillaJiraClient(HttpClient httpClient, string userName)
     public static async Task CheckHttpResponseForErrorMessages(HttpResponseMessage responseMessage)
     {
         using Stream responseContentStream = await responseMessage.Content.ReadAsStreamAsync();
-        JiraRestResponse responseContent = await HttpClientJsonExt.DeserializeJsonStreamAsync<JiraRestResponse>(responseContentStream);
-        if (responseContent?.ErrorMessages is not null && responseContent.ErrorMessages.Any())
-            throw new InvalidOperationException(string.Join(Environment.NewLine, responseContent.ErrorMessages));
+
+        if (responseContentStream.Length > 0)
+        {
+            JiraRestResponse responseContent = await HttpClientJsonExt.DeserializeJsonStreamAsync<JiraRestResponse>(responseContentStream);
+            if (responseContent?.ErrorMessages is not null && responseContent.ErrorMessages.Any())
+                throw new InvalidOperationException(string.Join(Environment.NewLine, responseContent.ErrorMessages));
+        }
     }
 
     public async Task<api.rest.common.JiraUserInfo> GetUserInfo()

From 8ba0c4beec13701c2efe2a2abdf91a0010eae894 Mon Sep 17 00:00:00 2001
From: nop77svk <nop77svk@gmail.com>
Date: Wed, 31 Jan 2024 14:22:48 +0100
Subject: [PATCH 25/28] =?UTF-8?q?=F0=9F=94=A7=20release=20pipeline=20updat?=
 =?UTF-8?q?ed?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .github/workflows/release.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 23c0058..f12de20 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -62,6 +62,7 @@ jobs:
       uses: softprops/action-gh-release@v1
       with:
         token: "${{ secrets.GITHUB_TOKEN }}"
+        body: "<changelog should come here>"
         prerelease: false
         files: |
           ./_publish.all/jira-worklogger.*

From 0cb460bbbf3bed9aa741d027e594ff6698b01640 Mon Sep 17 00:00:00 2001
From: nop77svk <nop77svk@gmail.com>
Date: Wed, 31 Jan 2024 14:29:51 +0100
Subject: [PATCH 26/28] update release pipeline

---
 .github/workflows/release.yml | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f12de20..dfa44d7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,10 +13,6 @@ jobs:
         dotnet-version: 6.0.x
     - name: Checkout
       uses: actions/checkout@v3
-    - name: Verify commit exists in origin/main
-      run: |
-        git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
-        git branch --remote --contains | grep origin/main
     - name: Set VERSION variable from tag
       run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
     - name: Update private NuGet source

From 1548254b689e98e187c2bfdf2a3f114f2f73ea67 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?=
 <16204822+nop77svk@users.noreply.github.com>
Date: Wed, 31 Jan 2024 15:55:07 +0000
Subject: [PATCH 27/28] Update README.md (#8)

---
 README.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index a33d709..7059ade 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,10 @@ Meet the scripted Jira worklogging! Give it your worklogs in a CSV file (and you
 ## Prerequisites
 
 - .NET 6 run-time installed (for simple, cross-platform build) or no .NET runtime necessary (for self-contained, single-exe, Windows-only build); You choose!
-- Jira server (with version 2 REST API) and the "Tempo Timesheets" plugin
+- Jira server (with version 2 REST API)
+-- "vanilla" Jira server support: ✔️
+-- "Tempo Timesheets" plugin support: ✔️
+-- "ICTime" plugin support: ❎ (planned)
 
 ## Configuration
 

From 5c226cfb8caf32808db94862e532fcbbdd9e6a56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20Hra=C5=A1ko?=
 <16204822+nop77svk@users.noreply.github.com>
Date: Wed, 31 Jan 2024 16:01:49 +0000
Subject: [PATCH 28/28] =?UTF-8?q?=F0=9F=9B=A0=20update=20README=20(#9)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/README.md b/README.md
index 7059ade..6f17442 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,9 @@ Meet the scripted Jira worklogging! Give it your worklogs in a CSV file (and you
 
 - .NET 6 run-time installed (for simple, cross-platform build) or no .NET runtime necessary (for self-contained, single-exe, Windows-only build); You choose!
 - Jira server (with version 2 REST API)
--- "vanilla" Jira server support: ✔️
--- "Tempo Timesheets" plugin support: ✔️
--- "ICTime" plugin support: ❎ (planned)
+  - "vanilla" Jira server support: ✔️
+  - "Tempo Timesheets" plugin support: ✔️
+  - "ICTime" plugin support: ❎ (planned)
 
 ## Configuration