Archive 5 phases (36 plans) to milestones/v1.0-phases/. Archive roadmap, requirements, and audit to milestones/. Evolve PROJECT.md with shipped state and validated requirements. Collapse ROADMAP.md to one-line milestone summary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
12 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-permissions | 04 | execute | 2 |
|
|
true |
|
|
Purpose: Deliver PERM-05 (CSV export) and PERM-06 (HTML export). These are pure data-transformation services with no UI dependency — they can be verified fully by the automated test stubs created in Plan 01. Output: 2 export service files.
<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/phases/02-permissions/02-RESEARCH.md From SharepointToolbox/Core/Models/PermissionEntry.cs: ```csharp public record PermissionEntry( string ObjectType, // "Site Collection" | "Site" | "List" | "Folder" string Title, string Url, bool HasUniquePermissions, string Users, // Semicolon-joined display names string UserLogins, // Semicolon-joined login names string PermissionLevels, // Semicolon-joined role names string GrantedThrough, // "Direct Permissions" | "SharePoint Group: " string PrincipalType // "SharePointGroup" | "User" | "External User" ); ```CSV merge logic (port of PS Merge-PermissionRows):
- Group by key: (Users, PermissionLevels, GrantedThrough)
- For each group: collect all Urls, join with " | "
- Collect all Titles, join with " | "
- Take first ObjectType, HasUniquePermissions from group
CSV columns (9 total): Object, Title, URL, HasUniquePermissions, Users, UserLogins, Type, Permissions, GrantedThrough CSV escaping: enclose every field in double quotes, escape internal quotes by doubling them.
HTML report key features (port of PS Export-PermissionsToHTML):
- Stats cards: Total Entries (count of entries), Unique Permission Sets (count of distinct PermissionLevels values), Distinct Users/Groups (count of distinct users across all UserLogins)
- Filter input (vanilla JS filterTable())
- Type badge: color-coded span for ObjectType ("Site Collection"=blue, "Site"=green, "List"=yellow, "Folder"=gray)
- Unique vs Inherited badge per row (HasUniquePermissions → green "Unique", else gray "Inherited")
- User pills with data-email attribute for each login in UserLogins (split by ;)
- Self-contained: all CSS and JS inline in the HTML string — no external file dependencies
- Table columns: Object, Title, URL, Unique, Users, Permissions, Granted Through
```csharp
namespace SharepointToolbox.Services.Export;
public class CsvExportService
{
private const string Header =
"\"Object\",\"Title\",\"URL\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\"";
public string BuildCsv(IReadOnlyList<PermissionEntry> entries)
{
var sb = new StringBuilder();
sb.AppendLine(Header);
// Merge: group by (Users, PermissionLevels, GrantedThrough)
var merged = entries
.GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough))
.Select(g => new
{
ObjectType = g.First().ObjectType,
Title = string.Join(" | ", g.Select(e => e.Title).Distinct()),
Url = string.Join(" | ", g.Select(e => e.Url).Distinct()),
HasUnique = g.First().HasUniquePermissions,
Users = g.Key.Users,
UserLogins = g.First().UserLogins,
PrincipalType= g.First().PrincipalType,
Permissions = g.Key.PermissionLevels,
GrantedThrough = g.Key.GrantedThrough
});
foreach (var row in merged)
sb.AppendLine(string.Join(",", new[]
{
Csv(row.ObjectType), Csv(row.Title), Csv(row.Url),
Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins),
Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.GrantedThrough)
}));
return sb.ToString();
}
public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
{
var csv = BuildCsv(entries);
await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
}
private static string Csv(string value)
{
if (string.IsNullOrEmpty(value)) return "\"\"";
return $"\"{value.Replace("\"", "\"\"")}\"";
}
}
```
Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`.
Namespace: `SharepointToolbox.Services.Export`.
dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~CsvExportServiceTests" -x
All 3 CsvExportServiceTests pass (header row present, merge works, empty list returns header only). dotnet build 0 errors.
Task 2: Implement HtmlExportService
SharepointToolbox/Services/Export/HtmlExportService.cs
- BuildHtml(IReadOnlyList<PermissionEntry> entries) returns a self-contained HTML string
- Output contains user display names from the input (test: BuildHtml_WithKnownEntries_ContainsUserNames passes)
- Output contains all inline CSS and JS — no <link> or <script src=...> tags
- Stats cards reflect: Total Entries count, Unique Permission Sets (distinct PermissionLevels values), Distinct Users (distinct entries in UserLogins split by semicolon)
- Type badge CSS classes: site-coll, site, list, folder — color-coded
- Unique/Inherited badge based on HasUniquePermissions
- Filter input calls JS filterTable() on keyup — filters by any visible text in the row
- External user tag: if UserLogins contains "#EXT#", user pill gets class "external-user" and data-email attribute
- WriteAsync(entries, filePath, ct) writes UTF-8 (no BOM for HTML)
- The test from Plan 01 (BuildHtml_WithExternalUser_ContainsExtHashMarker) passes — HTML contains "external-user" class
Create `SharepointToolbox/Services/Export/HtmlExportService.cs`.
Structure the HTML report as a multi-line C# string literal inside BuildHtml(). Use `StringBuilder` to assemble:
1. HTML head (with inline CSS): table styles, badge styles (site-coll=blue, site=green, list=amber, folder=gray, unique=green, inherited=gray), user pill styles, external-user pill style (orange border), stats card styles, filter input style
2. Body open: h1 "SharePoint Permissions Report", stats cards div (compute counts from entries), filter input
3. Table with columns: Object | Title | URL | Unique | Users/Groups | Permission Level | Granted Through
4. For each entry: one `<tr>` with:
- `<td><span class="{objectTypeCss}">{ObjectType}</span></td>`
- `<td>{Title}</td>`
- `<td><a href="{Url}" target="_blank">Link</a></td>`
- `<td><span class="{uniqueCss}">{Unique/Inherited}</span></td>`
- `<td>` + user pills: split UserLogins by ';', split Users by ';', zip them, render `<span class="user-pill {externalClass}" data-email="{login}">{name}</span>`
- `<td>{PermissionLevels}</td>`
- `<td>{GrantedThrough}</td>`
5. Inline JS: filterTable() function that iterates `<tr>` elements and shows/hides based on input text match against `tr.textContent`
6. Close body/html
Helper method `private static string ObjectTypeCss(string t)`:
- "Site Collection" → "badge site-coll"
- "Site" → "badge site"
- "List" → "badge list"
- "Folder" → "badge folder"
- else → "badge"
Stats computation:
```csharp
var totalEntries = entries.Count;
var uniquePermSets = entries.Select(e => e.PermissionLevels).Distinct().Count();
var distinctUsers = entries.SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries))
.Select(u => u.Trim()).Where(u => u.Length > 0).Distinct().Count();
```
Namespace: `SharepointToolbox.Services.Export`.
Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`.
dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~HtmlExportServiceTests" -x
All 3 HtmlExportServiceTests pass (user name present, empty list produces valid HTML, external user gets external-user class). dotnet build 0 errors.
- `dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx` → all tests pass (Phase 1 + new export tests)
- CsvExportServiceTests: 3 green
- HtmlExportServiceTests: 3 green
- HTML output contains no external script/link tags (grep verifiable: no `src=` or `href=` outside the table)
<success_criteria>
- CsvExportService merges rows by (Users, PermissionLevels, GrantedThrough) before writing
- CSV uses UTF-8 with BOM for Excel compatibility
- HtmlExportService produces self-contained HTML with inline CSS and JS
- HTML correctly marks external users with "external-user" CSS class
- All 6 export tests pass (3 CSV + 3 HTML) </success_criteria>