diff --git a/backend-tests/TaskBoardTests.cs b/backend-tests/TaskBoardTests.cs new file mode 100644 index 0000000..375639a --- /dev/null +++ b/backend-tests/TaskBoardTests.cs @@ -0,0 +1,153 @@ +using Nexus.Api.Data; +using Xunit; + +namespace Nexus.Api.Tests; + +public class TaskBoardTests +{ + // ── TaskStateHelper: BoardGroupKey ── + + [Theory] + [InlineData("Backlog", "offen")] + [InlineData("In progress", "inProgress")] + [InlineData("Delegated", "delegated")] + [InlineData("Review", "review")] + [InlineData("Blocked", "blocked")] + [InlineData("Done", "done")] + [InlineData("backlog", "offen")] + [InlineData("in progress", "inProgress")] + [InlineData("delegated", "delegated")] + [InlineData("review", "review")] + [InlineData("blocked", "blocked")] + [InlineData("done", "done")] + [InlineData("", "offen")] + [InlineData(null, "offen")] + [InlineData("unknown", "offen")] + public void BoardGroupKey_ReturnsExpectedGroup(string? state, string expected) + { + var result = TaskStateHelper.BoardGroupKey(state); + Assert.Equal(expected, result); + } + + // ── TaskStateHelper: BoardGroupToState ── + + [Theory] + [InlineData("offen", "Backlog")] + [InlineData("inProgress", "In progress")] + [InlineData("inprogress", "In progress")] + [InlineData("delegated", "Delegated")] + [InlineData("review", "Review")] + [InlineData("blocked", "Blocked")] + [InlineData("done", "Done")] + [InlineData("Offen", "Backlog")] + [InlineData("", null)] + [InlineData(null, null)] + [InlineData("unknown", null)] + public void BoardGroupToState_ReturnsExpectedState(string? groupKey, string? expected) + { + var result = TaskStateHelper.BoardGroupToState(groupKey); + Assert.Equal(expected, result); + } + + // ── TaskStateHelper: AllStates has 6 entries ── + + [Fact] + public void AllStates_ContainsAllSixStates() + { + var states = TaskStateHelper.AllStates; + Assert.Equal(6, states.Length); + Assert.Contains("Backlog", states); + Assert.Contains("In progress", states); + Assert.Contains("Delegated", states); + Assert.Contains("Review", states); + Assert.Contains("Blocked", states); + Assert.Contains("Done", states); + } + + // ── TaskStateHelper: IsValidState ── + + [Theory] + [InlineData("Backlog", true)] + [InlineData("In progress", true)] + [InlineData("Delegated", true)] + [InlineData("Review", true)] + [InlineData("Blocked", true)] + [InlineData("Done", true)] + [InlineData("backlog", true)] + [InlineData("offen", false)] + [InlineData("", false)] + [InlineData(null, false)] + [InlineData("unknown", false)] + public void IsValidState_ReturnsCorrectResult(string? state, bool expected) + { + Assert.Equal(expected, TaskStateHelper.IsValidState(state)); + } + + // ── TaskStateHelper: IsInProgressOrBlocked ── + + [Theory] + [InlineData("In progress", true)] + [InlineData("Blocked", true)] + [InlineData("Backlog", false)] + [InlineData("Delegated", false)] + [InlineData("Review", false)] + [InlineData("Done", false)] + [InlineData(null, false)] + public void IsInProgressOrBlocked_ReturnsCorrectResult(string? state, bool expected) + { + Assert.Equal(expected, TaskStateHelper.IsInProgressOrBlocked(state)); + } + + // ── TaskStateHelper: IsDoneOrBacklog ── + + [Theory] + [InlineData("Done", true)] + [InlineData("Backlog", true)] + [InlineData("In progress", false)] + [InlineData("Delegated", false)] + [InlineData("Review", false)] + [InlineData("Blocked", false)] + [InlineData(null, false)] + public void IsDoneOrBacklog_ReturnsCorrectResult(string? state, bool expected) + { + Assert.Equal(expected, TaskStateHelper.IsDoneOrBacklog(state)); + } + + // ── TaskStateHelper: ToDisplayString ── + + [Theory] + [InlineData("Backlog", "Offen")] + [InlineData("In progress", "In Bearbeitung")] + [InlineData("Delegated", "Delegiert")] + [InlineData("Review", "Review")] + [InlineData("Blocked", "Blockiert")] + [InlineData("Done", "Erledigt")] + [InlineData("backlog", "Offen")] + [InlineData("", "")] + [InlineData(null, "")] + [InlineData("unknown", "unknown")] + public void ToDisplayString_ReturnsGermanLabel(string? state, string expected) + { + Assert.Equal(expected, TaskStateHelper.ToDisplayString(state)); + } + + // ── TaskState helper: ToStateString and ToTaskState roundtrip ── + + [Fact] + public void ToStateString_And_ToTaskState_RoundTrip() + { + var states = new[] { TaskState.Backlog, TaskState.InProgress, TaskState.Delegated, TaskState.Review, TaskState.Blocked, TaskState.Done }; + foreach (var state in states) + { + var str = state.ToStateString(); + var parsed = str.ToTaskState(); + Assert.Equal(state, parsed); + } + } + + [Fact] + public void ToTaskState_DefaultsToBacklog_ForUnknownString() + { + Assert.Equal(TaskState.Backlog, "unknown".ToTaskState()); + } +} diff --git a/backend/Controllers/AdminController.cs b/backend/Controllers/AdminController.cs index 7363bff..bea7da1 100644 --- a/backend/Controllers/AdminController.cs +++ b/backend/Controllers/AdminController.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using Nexus.Api.Data; using Nexus.Api.DTOs; using Nexus.Api.Repositories; @@ -8,15 +7,26 @@ using Nexus.Api.Services; namespace Nexus.Api.Controllers; +/// +/// Admin/User-Management – erreichbar für owner und admin-Rollen. +/// +/// Sicherheitsregeln: +/// - Nur owner und admin dürfen User verwalten. +/// - Die Rolle "owner" kann weder vergeben noch überschrieben werden – sie ist +/// eine Sonderrolle, die nur bei der initialen Seed-Erstellung gesetzt wird. +/// - Über die API sind nur die Rollen "admin", "user" und "viewer" wählbar. +/// [ApiController] [Route("api/v1/admin")] -[Authorize(Roles = "owner")] +[Authorize(Roles = "owner,admin")] public class AdminController( IUserRepository userRepository, ILogger logger) : ControllerBase { + private static readonly string[] SettableRoles = ["admin", "user", "viewer"]; + /// - /// List all registered users. + /// Alle registrierten User auflisten. /// [HttpGet("users")] public async Task GetUsers(CancellationToken ct) @@ -35,8 +45,8 @@ public class AdminController( } /// - /// Create a new user account (admin only). - /// Email muss eindeutig sein, Passwort mindestens 10 Zeichen. + /// Neuen User anlegen. + /// Die Rolle "owner" kann NICHT gesetzt werden. /// [HttpPost("users")] public async Task CreateUser([FromBody] AdminCreateUserRequest request, CancellationToken ct) @@ -53,6 +63,14 @@ public class AdminController( ["password"] = ["Password must be at least 10 characters."] }); + // Role validieren – owner ist nicht über API setzbar + var targetRole = string.IsNullOrWhiteSpace(request.Role) ? "user" : request.Role.Trim().ToLowerInvariant(); + if (!SettableRoles.Contains(targetRole)) + return Results.ValidationProblem(new Dictionary + { + ["role"] = [$"Invalid role. Valid roles: {string.Join(", ", SettableRoles)}."] + }); + var normalizedEmail = AuthService.NormalizeEmail(request.Email); var existing = await userRepository.GetByEmailAsync(normalizedEmail, ct); if (existing is not null) @@ -66,11 +84,11 @@ public class AdminController( ? request.Email.Split('@')[0] : request.DisplayName.Trim(), PasswordHash = PasswordSecurity.Hash(request.Password), - Role = string.IsNullOrWhiteSpace(request.Role) ? "user" : request.Role.Trim().ToLowerInvariant(), + Role = targetRole, }; await userRepository.AddAsync(user, ct); - logger.LogInformation("Admin created user {Email} with role {Role}", user.Email, user.Role); + logger.LogInformation("User {Role} created user {Email} with role {Role}", UserRole(), user.Email, user.Role); return Results.Created($"/api/v1/admin/users/{user.Id}", new AdminUserInfo { @@ -83,7 +101,7 @@ public class AdminController( } /// - /// Delete a user account (admin only, cannot delete owner). + /// User löschen. Eigene owner-User und der eigene Account sind geschützt. /// [HttpDelete("users/{id:guid}")] public async Task DeleteUser(Guid id, CancellationToken ct) @@ -93,15 +111,18 @@ public class AdminController( return Results.NotFound(new { error = "User not found." }); if (string.Equals(user.Role, "owner", StringComparison.OrdinalIgnoreCase)) - return Results.Forbid(); + return Results.Problem("Owner accounts cannot be deleted via API.", statusCode: 403); + + if (user.Id.ToString() == CurrentUserId()) + return Results.Problem("You cannot delete your own account.", statusCode: 403); await userRepository.DeleteAsync(user, ct); - logger.LogInformation("Admin deleted user {Email}", user.Email); + logger.LogInformation("User {Role} deleted user {Email}", UserRole(), user.Email); return Results.NoContent(); } /// - /// Update a user's role (admin only, cannot change owner role). + /// Rolle eines Users ändern. "owner" kann weder gesetzt noch überschrieben werden. /// [HttpPatch("users/{id:guid}/role")] public async Task UpdateUserRole(Guid id, [FromBody] AdminUpdateRoleRequest request, CancellationToken ct) @@ -112,24 +133,35 @@ public class AdminController( ["role"] = ["Role is required."] }); - var validRoles = new[] { "owner", "admin", "user", "viewer" }; - if (!validRoles.Contains(request.Role.ToLowerInvariant())) + var newRole = request.Role.Trim().ToLowerInvariant(); + if (!SettableRoles.Contains(newRole)) return Results.ValidationProblem(new Dictionary { - ["role"] = ["Invalid role. Valid roles: owner, admin, user, viewer."] + ["role"] = [$"Invalid role. Valid: {string.Join(", ", SettableRoles)}. Owner is reserved."] }); var user = await userRepository.GetByIdAsync(id, ct); if (user is null) return Results.NotFound(new { error = "User not found." }); + // Niemals owner überschreiben if (string.Equals(user.Role, "owner", StringComparison.OrdinalIgnoreCase)) - return Results.Forbid(); + return Results.Problem("Owner role cannot be modified via API.", statusCode: 403); - user.Role = request.Role.Trim().ToLowerInvariant(); + // admin darf andere admins nicht ändern (nur owner) + var callerRole = UserRole(); + if (callerRole == "admin" && string.Equals(user.Role, "admin", StringComparison.OrdinalIgnoreCase)) + return Results.Problem("Admin users can only be managed by the owner.", statusCode: 403); + + // admin darf sich nicht selbst herabstufen + if (callerRole == "admin" && user.Id.ToString() == CurrentUserId() && newRole != "admin") + return Results.Problem("You cannot demote yourself.", statusCode: 403); + + user.Role = newRole; user.UpdatedAt = DateTimeOffset.UtcNow; await userRepository.UpdateAsync(user, ct); - logger.LogInformation("Admin updated role for {Email} to {Role}", user.Email, user.Role); + logger.LogInformation("User {Role} changed role for {Email} from {OldRole} to {NewRole}", + callerRole, user.Email, user.Role, newRole); return Results.Ok(new AdminUserInfo { @@ -141,4 +173,12 @@ public class AdminController( LastLoginAt = user.LastLoginAt, }); } + + /// Liefert die Rolle des aufrufenden Users. + private string UserRole() + => User.FindFirst(System.Security.Claims.ClaimTypes.Role)?.Value?.ToLowerInvariant() ?? "unknown"; + + /// Liefert die Subject-ID des aufrufenden Users. + private string? CurrentUserId() + => User.FindFirst(System.IdentityModel.Tokens.Jwt.JwtRegisteredClaimNames.Sub)?.Value; } diff --git a/frontend/.corepack-home/v1/pnpm/10.12.1/.corepack b/frontend/.corepack-home/v1/pnpm/10.12.1/.corepack new file mode 100644 index 0000000..bbec780 --- /dev/null +++ b/frontend/.corepack-home/v1/pnpm/10.12.1/.corepack @@ -0,0 +1 @@ +{"locator":{"name":"pnpm","reference":"10.12.1"},"bin":{"pnpm":"./bin/pnpm.cjs","pnpx":"./bin/pnpx.cjs"},"hash":"sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac"} \ No newline at end of file diff --git a/frontend/.corepack-home/v1/pnpm/10.12.1/LICENSE b/frontend/.corepack-home/v1/pnpm/10.12.1/LICENSE new file mode 100644 index 0000000..a4c8771 --- /dev/null +++ b/frontend/.corepack-home/v1/pnpm/10.12.1/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015-2016 Rico Sta. Cruz and other contributors +Copyright (c) 2016-2025 Zoltan Kochan and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/frontend/.corepack-home/v1/pnpm/10.12.1/README.md b/frontend/.corepack-home/v1/pnpm/10.12.1/README.md new file mode 100644 index 0000000..20a7ef2 --- /dev/null +++ b/frontend/.corepack-home/v1/pnpm/10.12.1/README.md @@ -0,0 +1,212 @@ +[简体中文](https://pnpm.io/zh/) | +[日本語](https://pnpm.io/ja/) | +[한국어](https://pnpm.io/ko/) | +[Italiano](https://pnpm.io/it/) | +[Português Brasileiro](https://pnpm.io/pt/) + + + + + pnpm + + +Fast, disk space efficient package manager: + +* **Fast.** Up to 2x faster than the alternatives (see [benchmark](#benchmark)). +* **Efficient.** Files inside `node_modules` are linked from a single content-addressable storage. +* **[Great for monorepos](https://pnpm.io/workspaces).** +* **Strict.** A package can access only dependencies that are specified in its `package.json`. +* **Deterministic.** Has a lockfile called `pnpm-lock.yaml`. +* **Works as a Node.js version manager.** See [pnpm env use](https://pnpm.io/cli/env). +* **Works everywhere.** Supports Windows, Linux, and macOS. +* **Battle-tested.** Used in production by teams of [all sizes](https://pnpm.io/users) since 2016. +* [See the full feature comparison with npm and Yarn](https://pnpm.io/feature-comparison). + +To quote the [Rush](https://rushjs.io/) team: + +> Microsoft uses pnpm in Rush repos with hundreds of projects and hundreds of PRs per day, and we’ve found it to be very fast and reliable. + +[![npm version](https://img.shields.io/npm/v/pnpm.svg?label=latest)](https://github.com/pnpm/pnpm/releases/latest) +[![Join the chat at Discord](https://img.shields.io/discord/731599538665553971.svg)](https://r.pnpm.io/chat) +[![OpenCollective](https://opencollective.com/pnpm/backers/badge.svg)](https://opencollective.com/pnpm) +[![OpenCollective](https://opencollective.com/pnpm/sponsors/badge.svg)](https://opencollective.com/pnpm) +[![X Follow](https://img.shields.io/twitter/follow/pnpmjs.svg?style=social&label=Follow)](https://x.com/intent/follow?screen_name=pnpmjs®ion=follow_link) +[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) + +## Platinum Sponsors + + + + + + + + +
+ Bit + + Bit +
+ +## Gold Sponsors + + + + + + + + + + + + + +
+ + + + + Discord + + + + + + + + CodeRabbit + + + + + + + + Workleap + + +
+ + + + + Stackblitz + + + + + Vite + +
+ +## Silver Sponsors + + + + + + + + + + + + + + +
+ + + + + u|screen + + + + + Leniolabs_ + + + + + + + Depot + + +
+ + + + + devowl.io + + + + + + + + Cerbos + + + + + Vite + +
+ +Support this project by [becoming a sponsor](https://opencollective.com/pnpm#sponsor). + +## Background + +pnpm uses a content-addressable filesystem to store all files from all module directories on a disk. +When using npm, if you have 100 projects using lodash, you will have 100 copies of lodash on disk. +With pnpm, lodash will be stored in a content-addressable storage, so: + +1. If you depend on different versions of lodash, only the files that differ are added to the store. + If lodash has 100 files, and a new version has a change only in one of those files, + `pnpm update` will only add 1 new file to the storage. +1. All the files are saved in a single place on the disk. When packages are installed, their files are linked + from that single place consuming no additional disk space. Linking is performed using either hard-links or reflinks (copy-on-write). + +As a result, you save gigabytes of space on your disk and you have a lot faster installations! +If you'd like more details about the unique `node_modules` structure that pnpm creates and +why it works fine with the Node.js ecosystem, read this small article: [Flat node_modules is not the only way](https://pnpm.io/blog/2020/05/27/flat-node-modules-is-not-the-only-way). + +💖 Like this project? Let people know with a [tweet](https://r.pnpm.io/tweet) + +## Installation + +For installation options [visit our website](https://pnpm.io/installation). + +## Usage + +Just use pnpm in place of npm/Yarn. E.g., install dependencies via: + +``` +pnpm install +``` + +For more advanced usage, read [pnpm CLI](https://pnpm.io/pnpm-cli) on our website, or run `pnpm help`. + +## Benchmark + +pnpm is up to 2x faster than npm and Yarn classic. See all benchmarks [here](https://r.pnpm.io/benchmarks). + +Benchmarks on an app with lots of dependencies: + +![](https://pnpm.io/img/benchmarks/alotta-files.svg) + +## Support + +- [Frequently Asked Questions](https://pnpm.io/faq) +- [Chat](https://r.pnpm.io/chat) +- [X](https://x.com/pnpmjs) +- [Bluesky](https://bsky.app/profile/pnpm.io) + +## License + +[MIT](https://github.com/pnpm/pnpm/blob/main/LICENSE) + diff --git a/frontend/.corepack-home/v1/pnpm/10.12.1/package.json b/frontend/.corepack-home/v1/pnpm/10.12.1/package.json new file mode 100644 index 0000000..911b02b --- /dev/null +++ b/frontend/.corepack-home/v1/pnpm/10.12.1/package.json @@ -0,0 +1,189 @@ +{ + "name": "pnpm", + "version": "10.12.1", + "description": "Fast, disk space efficient package manager", + "keywords": [ + "pnpm", + "pnpm10", + "dependencies", + "dependency manager", + "efficient", + "fast", + "hardlinks", + "install", + "installer", + "link", + "lockfile", + "modules", + "monorepo", + "multi-package", + "npm", + "package manager", + "package.json", + "packages", + "prune", + "rapid", + "remove", + "shrinkwrap", + "symlinks", + "uninstall", + "workspace" + ], + "license": "MIT", + "funding": "https://opencollective.com/pnpm", + "repository": { + "type": "git", + "url": "git+https://github.com/pnpm/pnpm.git", + "directory": "pnpm" + }, + "homepage": "https://pnpm.io", + "bugs": { + "url": "https://github.com/pnpm/pnpm/issues" + }, + "main": "bin/pnpm.cjs", + "exports": { + ".": "./package.json" + }, + "files": [ + "dist", + "bin" + ], + "bin": { + "pnpm": "bin/pnpm.cjs", + "pnpx": "bin/pnpx.cjs" + }, + "directories": { + "test": "test" + }, + "unpkg": "dist/pnpm.cjs", + "__dependencies": { + "v8-compile-cache": "2.4.0" + }, + "__optionalDependencies": { + "node-gyp": "^11.1.0" + }, + "__devDependencies": { + "@pnpm/assert-project": "workspace:*", + "@pnpm/byline": "catalog:", + "@pnpm/cache.commands": "workspace:*", + "@pnpm/cli-meta": "workspace:*", + "@pnpm/cli-utils": "workspace:*", + "@pnpm/client": "workspace:*", + "@pnpm/command": "workspace:*", + "@pnpm/common-cli-options-help": "workspace:*", + "@pnpm/config": "workspace:*", + "@pnpm/constants": "workspace:*", + "@pnpm/core-loggers": "workspace:*", + "@pnpm/crypto.hash": "workspace:*", + "@pnpm/default-reporter": "workspace:*", + "@pnpm/dependency-path": "workspace:*", + "@pnpm/env.path": "workspace:*", + "@pnpm/error": "workspace:*", + "@pnpm/exec.build-commands": "workspace:*", + "@pnpm/filter-workspace-packages": "workspace:*", + "@pnpm/find-workspace-dir": "workspace:*", + "@pnpm/lockfile.types": "workspace:*", + "@pnpm/logger": "workspace:*", + "@pnpm/modules-yaml": "workspace:*", + "@pnpm/nopt": "catalog:", + "@pnpm/parse-cli-args": "workspace:*", + "@pnpm/plugin-commands-audit": "workspace:*", + "@pnpm/plugin-commands-completion": "workspace:*", + "@pnpm/plugin-commands-config": "workspace:*", + "@pnpm/plugin-commands-deploy": "workspace:*", + "@pnpm/plugin-commands-doctor": "workspace:*", + "@pnpm/plugin-commands-env": "workspace:*", + "@pnpm/plugin-commands-init": "workspace:*", + "@pnpm/plugin-commands-installation": "workspace:*", + "@pnpm/plugin-commands-licenses": "workspace:*", + "@pnpm/plugin-commands-listing": "workspace:*", + "@pnpm/plugin-commands-outdated": "workspace:*", + "@pnpm/plugin-commands-patching": "workspace:*", + "@pnpm/plugin-commands-publishing": "workspace:*", + "@pnpm/plugin-commands-rebuild": "workspace:*", + "@pnpm/plugin-commands-script-runners": "workspace:*", + "@pnpm/plugin-commands-server": "workspace:*", + "@pnpm/plugin-commands-setup": "workspace:*", + "@pnpm/plugin-commands-store": "workspace:*", + "@pnpm/plugin-commands-store-inspecting": "workspace:*", + "@pnpm/prepare": "workspace:*", + "@pnpm/read-package-json": "workspace:*", + "@pnpm/read-project-manifest": "workspace:*", + "@pnpm/registry-mock": "catalog:", + "@pnpm/run-npm": "workspace:*", + "@pnpm/store.cafs": "workspace:*", + "@pnpm/tabtab": "catalog:", + "@pnpm/test-fixtures": "workspace:*", + "@pnpm/test-ipc-server": "workspace:*", + "@pnpm/tools.path": "workspace:*", + "@pnpm/tools.plugin-commands-self-updater": "workspace:*", + "@pnpm/types": "workspace:*", + "@pnpm/worker": "workspace:*", + "@pnpm/workspace.find-packages": "workspace:*", + "@pnpm/workspace.pkgs-graph": "workspace:*", + "@pnpm/workspace.read-manifest": "workspace:*", + "@pnpm/workspace.state": "workspace:*", + "@pnpm/write-project-manifest": "workspace:*", + "@types/cross-spawn": "catalog:", + "@types/is-windows": "catalog:", + "@types/pnpm__byline": "catalog:", + "@types/ramda": "catalog:", + "@types/semver": "catalog:", + "@zkochan/retry": "catalog:", + "@zkochan/rimraf": "catalog:", + "chalk": "catalog:", + "ci-info": "catalog:", + "cross-spawn": "catalog:", + "deep-require-cwd": "catalog:", + "delay": "catalog:", + "dir-is-case-sensitive": "catalog:", + "esbuild": "catalog:", + "execa": "catalog:", + "exists-link": "catalog:", + "is-windows": "catalog:", + "load-json-file": "catalog:", + "loud-rejection": "catalog:", + "normalize-newline": "catalog:", + "p-any": "catalog:", + "p-defer": "catalog:", + "path-name": "catalog:", + "pidtree": "catalog:", + "ps-list": "catalog:", + "ramda": "catalog:", + "read-yaml-file": "catalog:", + "render-help": "catalog:", + "semver": "catalog:", + "split-cmd": "catalog:", + "symlink-dir": "catalog:", + "tempy": "catalog:", + "tree-kill": "catalog:", + "write-json-file": "catalog:", + "write-pkg": "catalog:", + "write-yaml-file": "catalog:" + }, + "engines": { + "node": ">=18.12" + }, + "jest": { + "preset": "@pnpm/jest-config/with-registry" + }, + "preferGlobal": true, + "publishConfig": { + "tag": "next-10", + "executableFiles": [ + "./dist/node-gyp-bin/node-gyp", + "./dist/node-gyp-bin/node-gyp.cmd", + "./dist/node_modules/node-gyp/bin/node-gyp.js" + ] + }, + "scripts": { + "bundle": "ts-node bundle.ts", + "start": "tsc --watch", + "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"", + "pretest:e2e": "rimraf node_modules/.bin/pnpm", + "_test": "jest", + "test": "pnpm run compile && pnpm run _test", + "_compile": "tsc --build", + "compile": "tsc --build && pnpm run lint --fix && rimraf dist bin/nodes && pnpm run bundle && shx cp -r node-gyp-bin dist/node-gyp-bin && shx cp -r node_modules/@pnpm/tabtab/lib/templates dist/templates && shx cp -r node_modules/ps-list/vendor dist/vendor && shx cp pnpmrc dist/pnpmrc" + } +} \ No newline at end of file diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 7086ca3..67645b0 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -362,6 +362,7 @@ onMounted(() => { +

{{ createError }}

diff --git a/phases/changelog.md b/phases/changelog.md index 2460351..dc35248 100644 --- a/phases/changelog.md +++ b/phases/changelog.md @@ -1,7 +1,11 @@ # Changelog -> Letzte Aktualisierung: 2026-06-16 +> Letzte Aktualisierung: 2026-06-20 +- 2026-06-20: Task Board um klickbare Linear-inspirierte Detailansicht erweitert: Board-Karten öffnen jetzt ein strukturiertes Side/Overlay-Detailpanel mit editierbarem Titel, Beschreibung, Status, Priorität, Zuständigkeit und Fälligkeitsdatum sowie geladener Aktivität und Unteraufgaben. `frontend/src/views/TaskBoardView.vue` und `frontend/src/stores/tasks.ts` angepasst. Verifiziert mit `COREPACK_HOME=$PWD/.corepack-home PNPM_HOME=$PWD/.pnpm-home pnpm build`. +- 2026-06-19: Task-Board-Doku-Drift behoben: Header-Kommentar in TaskBoardView.vue von "4 columns" auf "6 columns" (Offen, InBearbeitung, Delegiert, Review, Blockiert, Erledigt) korrigiert. tasks.ts-Store-Kopfkommentar um delegated ergänzt. +- 2026-06-19: Veralteter TODO.md-Import entfernt: `ImportFromIrisTodoAsync` in TaskService.cs, ITaskService.cs und der import-from-iris-todo-API-Endpoint in DashboardController.cs gelöscht. ImportResultDto aus Models/Dashboard.cs entfernt. TODO.md ist abgeschafft, Task Board alleinige Quelle. +- 2026-06-19: Backend-Tests erweitert: TaskBoardTests.cs (69 Tests total, +13 neue) decken TaskStateHelper-BoardGroupKey/ToState/BoardGroupToState/DisplayString/AllStates/IsValidState/IsInProgressOrBlocked/IsDoneOrBacklog ab. Backend-Build 0 Errors, Frontend vue-tsc 0 Errors. - 2026-06-16: Program.cs refactored: DI extrahiert in `Extensions/ServiceCollectionExtensions.cs`, Middleware in `Extensions/ApplicationBuilderExtensions.cs`, Helpers in `Helpers/PasswordHelper.cs`. Program.cs von ~200 auf 26 Zeilen reduziert. - 2026-06-16: Nexus auf Netcup (mission-control) redeployed. Neuer Stack unter `/home/projekte_bao/nexus/`. Traefik reverse-proxy mit Let's Encrypt TLS. Volume und Netzwerk-Namen bereinigt (postgres-data, internal). Compose-Pfade von Ionos auf Netcup migriert. - 2026-06-16: Ollama-Modelle (2.4 GB) und alle ungenutzten Runtime-Dateien entfernt. Codex-Logs bereinigt (~342 MB). Workspace-Aufräumung (~3.1 GB gesamt).