diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..0ec5db7 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,151 @@ +# Contributing to Profilarr + +Profilarr is a work-in-progress rewrite, so please coordinate larger changes first. This guide explains how the repo is organized and the expected contribution workflows. + +## Project Overview + +Profilarr is a SvelteKit + Deno app that manages and syncs configurations across \*arr apps using Profilarr Compliant Databases (PCDs). It compiles to standalone binaries. + +- **Frontend:** `src/routes/`, `src/lib/client/` +- **Backend:** `src/lib/server/` +- **PCDs:** git repositories cloned under `data/databases/` and compiled into an in-memory SQLite cache + +## Prerequisites + +- **Deno 2.x** +- **Node + npm** only if you want to run ESLint/Prettier (`deno task lint` or `deno task format`). +- **.NET 8** only if you work on the parser microservice in `services/parser/`. + +## Development Commands + +- `deno task dev` (default port 6969) +- `deno task test` +- `deno task lint` +- `deno task format` + +Useful environment variables: + +- `APP_BASE_PATH` (defaults to the compiled binary location) +- `PARSER_HOST`, `PARSER_PORT` (C# parser microservice) +- `PORT`, `HOST` + +## Repo Tour + +- `docs/ARCHITECTURE.md` — system overview +- `docs/PCD SPEC.md` — operational SQL & layering model +- `docs/manifest.md` — `pcd.json` schema +- `docs/PARSER_PORT_DESIGN.md` — parser microservice +- `services/parser/` — C# parser microservice + +## App Database vs PCD Databases + +**Profilarr app database** + +- SQLite file: `data/profilarr.db` +- Boot sequence initializes config, opens DB, runs migrations, starts job system. +- Migrations live in `src/lib/server/db/migrations/` and are run on startup. + +**PCD databases** + +- Git repos cloned into `data/databases/`. +- Compiled into an in-memory SQLite cache (`PCDCache`) using ordered SQL operations. +- Layers in order: `schema` → `base` → `tweaks` → `user`. +- SQL helper functions available inside PCD ops: `qp`, `cf`, `dp`, `tag`. + +## Adding a Migration + +1. Copy `src/lib/server/db/migrations/_template.ts` to a new file like `021_add_foo.ts`. +2. Update `version` and `name`, then fill out `up` SQL and (ideally) `down` SQL. +3. Add a static import in `src/lib/server/db/migrations.ts`. +4. Add the new migration to `loadMigrations()` (keep sequential ordering). + +Notes: + +- Versions must be unique and sequential. +- Never edit an applied migration; create a new one instead. +- Migrations run automatically on server startup. + +## Working with PCDs + +**PCD layout** + +``` +my-pcd/ +├── pcd.json +├── ops/ +└── tweaks/ +``` + +**Authoring operations** + +- Follow the append-only Operational SQL approach. +- Use expected-value guards in `UPDATE` statements to surface conflicts. +- New ops go in `ops/` or `tweaks/` depending on intent. + +**User ops** + +Profilarr writes user edits via `src/lib/server/pcd/writer.ts` into `user_ops/`, rebuilding the in-memory cache after write. + +## Client UI Components + +Shared UI lives in `src/lib/client/ui/`. Route-specific components live next to their routes. + +**Alerts and toasts** + +- Store: `src/lib/client/alerts/store.ts` +- Use the alert store for success/error/info toasts in `enhance` actions and API responses. + +**Actions and toolbars** + +- `src/lib/client/ui/actions/ActionsBar.svelte` +- `src/lib/client/ui/actions/ActionButton.svelte` +- `src/lib/client/ui/actions/SearchAction.svelte` +- `src/lib/client/ui/actions/ViewToggle.svelte` + +**Dropdowns** + +- `src/lib/client/ui/dropdown/Dropdown.svelte` +- `src/lib/client/ui/dropdown/DropdownItem.svelte` + +**Buttons** + +- `src/lib/client/ui/button/Button.svelte` (variants + sizes) + +**Forms** + +- `FormInput`, `NumberInput`, `TagInput`, `IconCheckbox` + +**Tables and lists** + +- `Table`, `ExpandableTable`, `ReorderableList` + +**Modals** + +- `Modal`, `SaveTargetModal`, `UnsavedChangesModal`, `InfoModal` + +**Navigation** + +- `navbar`, `pageNav`, `tabs` + +**State and empty views** + +- `EmptyState` + +## Svelte Conventions + +- Use Svelte 4 syntax (`export let`, `$:`) even though Svelte 5 is installed. +- Avoid Svelte 5 runes unless explicitly used in that module. +- Route-specific components should be colocated under their route directory. + +## Tests + +- Tests live in `src/tests/` and run with `deno task test`. +- Base test utilities are in `src/tests/base/BaseTest.ts`. +- Many tests create temp dirs under `/tmp/profilarr-tests`. + +## Parser Microservice (Optional) + +If you touch parser-related code, see `docs/PARSER_PORT_DESIGN.md` and `services/parser/`. + +- `dotnet run` from `services/parser/` +- Configure `PARSER_HOST` / `PARSER_PORT` in Profilarr diff --git a/services/parser/Program.cs b/services/parser/Program.cs index 77b165c..bb37a51 100644 --- a/services/parser/Program.cs +++ b/services/parser/Program.cs @@ -1,5 +1,9 @@ using Parser.Core; +// Bump this version when parser logic changes (regex patterns, parsing behavior, etc.) +// This invalidates the parse result cache in Profilarr +const string ParserVersion = "1.0.0"; + var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); @@ -93,7 +97,47 @@ app.MapPost("/parse", (ParseRequest request) => } }); -app.MapGet("/health", () => Results.Ok(new { status = "healthy" })); +app.MapGet("/health", () => Results.Ok(new { status = "healthy", version = ParserVersion })); + +app.MapPost("/match", (MatchRequest request) => +{ + if (string.IsNullOrWhiteSpace(request.Text)) + { + return Results.BadRequest(new { error = "Text is required" }); + } + + if (request.Patterns == null || request.Patterns.Count == 0) + { + return Results.BadRequest(new { error = "At least one pattern is required" }); + } + + var results = new Dictionary(); + + foreach (var pattern in request.Patterns) + { + try + { + var regex = new System.Text.RegularExpressions.Regex( + pattern, + System.Text.RegularExpressions.RegexOptions.IgnoreCase, + TimeSpan.FromMilliseconds(100) // Timeout to prevent ReDoS + ); + results[pattern] = regex.IsMatch(request.Text); + } + catch (System.Text.RegularExpressions.RegexMatchTimeoutException) + { + // Pattern took too long, treat as no match + results[pattern] = false; + } + catch (System.ArgumentException) + { + // Invalid regex pattern + results[pattern] = false; + } + } + + return Results.Ok(new MatchResponse { Results = results }); +}); app.Run(); @@ -140,3 +184,10 @@ public record EpisodeResponse public bool Special { get; init; } public string ReleaseType { get; init; } = "Unknown"; } + +public record MatchRequest(string Text, List Patterns); + +public record MatchResponse +{ + public Dictionary Results { get; init; } = new(); +} diff --git a/src/lib/client/ui/actions/SearchAction.svelte b/src/lib/client/ui/actions/SearchAction.svelte index 07d1aaa..62f8880 100644 --- a/src/lib/client/ui/actions/SearchAction.svelte +++ b/src/lib/client/ui/actions/SearchAction.svelte @@ -1,9 +1,14 @@
@@ -31,16 +49,24 @@
+ + {#if activeQuery} +
+ {activeQuery} +
+ {/if} + (isFocused = true)} on:blur={() => (isFocused = false)} - {placeholder} - class="h-full w-full bg-transparent pl-10 pr-10 text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400" + placeholder={activeQuery ? '' : placeholder} + class="h-full w-full bg-transparent pr-10 text-sm text-neutral-900 placeholder-neutral-500 outline-none dark:text-neutral-100 dark:placeholder-neutral-400 {activeQuery ? 'pl-2' : 'pl-10'}" /> @@ -51,6 +77,13 @@ > + {:else if activeQuery} + {/if} diff --git a/src/lib/client/ui/form/FormInput.svelte b/src/lib/client/ui/form/FormInput.svelte index 9d24c97..9cd256d 100644 --- a/src/lib/client/ui/form/FormInput.svelte +++ b/src/lib/client/ui/form/FormInput.svelte @@ -22,14 +22,14 @@ bind:value {placeholder} rows="6" - class="block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-900 placeholder-neutral-400 transition-colors focus:border-accent-500 focus:outline-none focus:ring-1 focus:ring-accent-500 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500" + class="block w-full rounded-lg border border-neutral-300 bg-white px-3 py-2 text-neutral-900 placeholder-neutral-400 transition-colors focus:border-neutral-400 focus:outline-none dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-50 dark:placeholder-neutral-500 dark:focus:border-neutral-500" > {:else} {/if} diff --git a/src/lib/client/ui/form/TagInput.svelte b/src/lib/client/ui/form/TagInput.svelte index ca5c5dc..f2166c0 100644 --- a/src/lib/client/ui/form/TagInput.svelte +++ b/src/lib/client/ui/form/TagInput.svelte @@ -1,5 +1,6 @@
{#each tags as tag, index (tag)} -
- {tag} + + {tag} -
+ {/each}
-
+

{header}

-
+

{bodyMessage}

@@ -78,7 +87,7 @@