Skip to content

Conversation

@ThePitter
Copy link

@ThePitter ThePitter commented Nov 5, 2025

Creates new Software Update page in Admin and enables administrators to chose to begin automated builds.
See #1249

Summary by CodeRabbit

  • New Features
    • Added software update administration interface enabling admins to manage and trigger software rebuilds across organizations.
    • New form allows admins to select target organizations and provide required comments when initiating updates.
    • Multilingual support added for software update feature in English, Spanish (Latin America), and French.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 5, 2025

Walkthrough

The changes introduce a new admin-gated software update feature enabling users to trigger product rebuilds across organizations. This includes localization keys, backend API extensions for comment propagation through workflows, a new authenticated route with product filtering logic, and an associated form UI.

Changes

Cohort / File(s) Change Summary
Localization Keys
src/lib/locales/en-US.json, src/lib/locales/es-419.json, src/lib/locales/fr-FR.json
Added seven new localization keys for software update admin interface: nav labels, descriptions, comments, rebuild prompts, validation messages, success notifications, and affected organization labels.
Workflow Comment Propagation
src/lib/server/workflow/index.ts, src/lib/products/server.ts
Extended workflow.create and doProductAction to accept an optional comment parameter; comment is persisted in workflow transitions and passed through rebuild/republish action calls.
Admin Navigation
src/routes/(authenticated)/+layout.svelte
Added new admin navigation item linking to /software-update route with active state detection and drawer closure on click.
Software Update Route (Backend)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
New route handler implementing admin-gated software update logic: resolves target organizations based on user roles, fetches products with RebuildOnSoftwareUpdate flag, filters by version requirements, and triggers rebuilds via doProductAction with user-provided comment.
Software Update Route (Frontend)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte
New Svelte page component rendering software update form with organization display, required comment input, and submit button; uses sveltekit-superforms for validation and progressive enhancement.

Sequence Diagram

sequenceDiagram
    actor Admin
    participant Page as Software Update Page
    participant Server as Route Handler
    participant DB as Database
    participant Workflow as Workflow Engine

    Admin->>Page: Load /software-update
    Page->>Server: GET (load)
    Server->>DB: Resolve admin status & target orgs
    Server->>DB: Fetch organization names
    Server->>Page: Return { form, organizations }
    Page->>Admin: Display form with org list & comment input

    Admin->>Page: Submit with comment
    Page->>Server: POST ?/start (form data + comment)
    Server->>DB: Validate admin rights (re-check)
    Server->>DB: Query products with RebuildOnSoftwareUpdate=true
    Server->>DB: Compare AppBuilderVersion vs SystemVersion
    
    loop For each eligible product
        Server->>Workflow: Call doProductAction(productId, Rebuild, comment)
        Workflow->>DB: Persist transition with comment
        Workflow->>Server: Acknowledge
    end
    
    Server->>Page: Return { form, ok: true }
    Page->>Admin: Show success toast
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts: Verify admin authorization logic, product filtering criteria (RebuildOnSoftwareUpdate, version comparisons), and correct invocation of doProductAction across multiple products with comment propagation.
  • Localization keys: Confirm all seven keys are consistently added across all three locale files with appropriate trailing commas in JSON syntax.
  • Workflow and product API changes: Ensure comment parameter threading through workflow.create and doProductAction is correctly typed and optional.

Possibly related PRs

Suggested reviewers

  • chrisvire
  • FyreByrd

Poem

🐰 A hop, a skip, soft-ware we go!
Comments flow through workflows, steady and slow,
Admins now rebuild with words to say,
Organizations updated, hip-hip-hooray! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing software update rebuild functionality across locales, server logic, workflow, routes, and UI components.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FyreByrd FyreByrd force-pushed the develop branch 2 times, most recently from 95e992b to 264d56f Compare November 6, 2025 17:19
@ThePitter ThePitter force-pushed the feature/software-update-rebuild branch from 7e5635e to 09bb94b Compare November 6, 2025 19:17
@ThePitter ThePitter requested a review from FyreByrd November 10, 2025 15:03
@ThePitter
Copy link
Author

Everything is setup but am uncertain where the comment is supposed to be stored?

@ThePitter ThePitter marked this pull request as ready for review November 10, 2025 15:04
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
src/routes/(authenticated)/admin/settings/software-update/+page.server.ts (1)

37-58: Consider filtering products to only rebuild those eligible for rebuild.

The query includes WorkflowInstance and ProductBuilds, but doesn't filter products that:

  • Already have an active workflow instance (shouldn't rebuild if already rebuilding)
  • Haven't completed initial publication yet

Consider adding a filter to exclude products already being rebuilt:

       include: {
         Products: {
+          where: {
+            WorkflowInstance: null
+          },
           include: {

Or add logic after the query to skip products with active workflows before calling doProductAction.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0af0aa and 80d6465.

📒 Files selected for processing (6)
  • src/lib/locales/en-US.json (1 hunks)
  • src/lib/locales/es-419.json (1 hunks)
  • src/lib/locales/fr-FR.json (1 hunks)
  • src/routes/(authenticated)/admin/settings/+layout.svelte (1 hunks)
  • src/routes/(authenticated)/admin/settings/software-update/+page.server.ts (1 hunks)
  • src/routes/(authenticated)/admin/settings/software-update/+page.svelte (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T14:07:02.200Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:285-303
Timestamp: 2025-09-12T14:07:02.200Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to optimize filterAvailableActions by creating Sets at the caller level rather than inside the function, so the function would take Set<number> arguments instead of arrays for better performance and separation of concerns.

Applied to files:

  • src/routes/(authenticated)/admin/settings/software-update/+page.server.ts
📚 Learning: 2025-09-12T14:57:08.647Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:252-264
Timestamp: 2025-09-12T14:57:08.647Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to use transition.target[0] prediction mechanism for determining post-send state rather than calling flow.state() after flow.send() due to race condition concerns. The prediction approach is sufficient for redirect decisions between staying on task page vs going to projects page.

Applied to files:

  • src/routes/(authenticated)/admin/settings/software-update/+page.server.ts
📚 Learning: 2025-09-22T14:59:53.366Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts:214-221
Timestamp: 2025-09-22T14:59:53.366Z
Learning: In the AppBuilder portal, bulk project actions in the route `/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts` are organization-scoped by design. The route parameter `[orgId=idNumber]` ensures all operations happen within a single organization context, so using the route-level orgId for all projects in bulk actions like `doProjectAction` is correct and safe.

Applied to files:

  • src/routes/(authenticated)/admin/settings/software-update/+page.server.ts
🧬 Code graph analysis (1)
src/routes/(authenticated)/admin/settings/software-update/+page.server.ts (2)
src/lib/server/database/prisma.ts (1)
  • DatabaseReads (22-22)
src/lib/products/server.ts (1)
  • doProductAction (7-88)
🪛 GitHub Actions: Test and Lint
src/routes/(authenticated)/admin/settings/+layout.svelte

[error] 21-21: Prettier formatting error reported by ESLint during 'npm run lint': Insert a space character (prettier/prettier) at column 68.

🪛 GitHub Check: checks / typecheck-lint
src/routes/(authenticated)/admin/settings/+layout.svelte

[failure] 21-21:
Insert ·

src/routes/(authenticated)/admin/settings/software-update/+page.svelte

[failure] 11-11:
Replace ········ with ····


[failure] 10-10:
Delete ··


[failure] 8-8:
Delete ··


[warning] 7-7:
$lib/paraglide/messages import should occur after import of $lib/components/settings/LabeledFormInput.svelte


[failure] 7-7:
Delete ··


[warning] 6-6:
'getLocale' is defined but never used


[warning] 6-6:
$lib/paraglide/runtime import should occur after import of $lib/components/settings/LabeledFormInput.svelte


[failure] 6-6:
Delete ··


[failure] 5-5:
Delete ··


[warning] 4-4:
$lib/utils import should occur after import of $lib/components/settings/LabeledFormInput.svelte


[failure] 4-4:
Replace ···· with ··


[failure] 3-3:
Delete ··


[warning] 2-2:
./$types type import should occur after import of sveltekit-superforms


[failure] 2-2:
Delete ··

🔇 Additional comments (3)
src/lib/locales/fr-FR.json (1)

551-557: LGTM! French localization keys added correctly.

The new localization keys for the software update feature are properly structured and the translations appear accurate.

src/lib/locales/es-419.json (1)

576-582: LGTM! Spanish localization keys added correctly.

The Spanish translations for the software update feature are properly structured and consistent with other locale files.

src/lib/locales/en-US.json (1)

576-582: LGTM! English localization keys properly defined.

The base English localization keys for the software update feature are clear and well-structured.

Comment on lines 37 to 65
const projects = await DatabaseReads.projects.findMany({
where: {
OrganizationId: orgIds ? { in: orgIds } : undefined,
DateArchived: null,
RebuildOnSoftwareUpdate: true
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify behavior when orgIds is an empty array.

If a super admin has no organization memberships (orgIds is an empty array), the query will use { in: [] }, which in Prisma matches nothing. This might be intentional, but confirm whether super admins should rebuild products across all organizations or none when they have no memberships.


🏁 Script executed:

#!/bin/bash
# Check how super admin organization access is handled elsewhere
rg -n -C5 'requireSuperAdmin|organizationMemberships' --type ts

Length of output: 62398


Fix inconsistent organization filter logic for super admins in software-update page.

The code calls requireSuperAdmin() but then filters by organizationMemberships like a regular user. This contradicts the established pattern in src/routes/(authenticated)/+layout.server.ts, where super admins get undefined (matching all organizations) instead of a membership-based filter.

If a super admin has no organization memberships, the current code will use { in: [] } and match no projects. It should match all projects instead. Follow the pattern used elsewhere: conditionally set orgIds = undefined for super admins to bypass the filter.

const orgIds: number[] | undefined = locals.security.isSuperAdmin 
  ? undefined 
  : locals.security.organizationMemberships;

Then adjust line 39 to use orgIds directly (the ternary already handles the undefined case).

🤖 Prompt for AI Agents
In src/routes/(authenticated)/admin/settings/software-update/+page.server.ts
around lines 37 to 42, the org filtering treats super admins like regular users
by passing an empty array into the query, causing no projects to match; change
the orgIds assignment so that if locals.security.isSuperAdmin is true set orgIds
= undefined, otherwise set orgIds = locals.security.organizationMemberships, and
then pass orgIds directly into the DatabaseReads.projects.findMany where clause
(remove the ternary that builds { in: orgIds } when orgIds may be undefined).

Comment on lines 61 to 183
await Promise.all(
projects.map((project) =>
project.Products.forEach((p) => doProductAction(p.Id, ProductActionType.Rebuild))
)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: forEach doesn't return promises, breaking async logic.

Promise.all expects an array of promises, but forEach returns undefined. The rebuild actions won't be awaited, and the response will be sent before the rebuilds are dispatched.

Apply this diff to fix the issue:

     // Await promises for all products running a doProductAction.
     await Promise.all(
-      projects.map((project) =>
-        project.Products.forEach((p) => doProductAction(p.Id, ProductActionType.Rebuild))
+      projects.flatMap((project) =>
+        project.Products.map((p) => doProductAction(p.Id, ProductActionType.Rebuild))
       )
     );

This uses flatMap to collect all projects and map (instead of forEach) to create an array of promises that can be properly awaited.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await Promise.all(
projects.map((project) =>
project.Products.forEach((p) => doProductAction(p.Id, ProductActionType.Rebuild))
)
);
await Promise.all(
projects.flatMap((project) =>
project.Products.map((p) => doProductAction(p.Id, ProductActionType.Rebuild))
)
);
🤖 Prompt for AI Agents
In src/routes/(authenticated)/admin/settings/software-update/+page.server.ts
around lines 61 to 65, the current Promise.all wraps projects.map(...forEach...)
where forEach returns undefined, so rebuild promises are never collected or
awaited; replace that logic by collecting an array of promises (e.g., use
projects.flatMap(p => p.Products.map(prod => doProductAction(prod.Id,
ProductActionType.Rebuild))) or use projects.map(...).flat() pattern) and pass
that array to Promise.all so all doProductAction calls are awaited.

Comment on lines 67 to 68
// TODO: The comment still needs to be stored somewhere.
// The comment value is at form.data.comment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Address the TODO: Comment storage implementation missing.

The form collects a comment (form.data.comment) but there's no implementation to persist it. Based on your PR comment asking "where the comment is supposed to be stored," this needs clarification.

Possible storage locations to consider:

  1. ProductTransitions table: Store with each rebuild transition (similar to how comments are stored for other workflow transitions)
  2. New SoftwareUpdate table: Track software update events with comments and metadata
  3. System log/audit table: Record admin actions with comments

Would you like me to help implement one of these approaches or generate a script to search for existing comment storage patterns in the codebase?


🏁 Script executed:

#!/bin/bash
# Search for how comments are currently stored in the codebase
rg -n -C3 'Comment.*:' --type-add 'prisma:*.prisma' --type prisma

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Step 1: Locate Prisma schema files
echo "=== Finding Prisma schema files ==="
fd -e prisma

# Step 2: Search for existing comment fields in schema
echo -e "\n=== Searching for comment-related fields ==="
rg -n "comment" --type prisma -i

# Step 3: Examine the specific file mentioned in review
echo -e "\n=== Content of target file ==="
cat src/routes/\(authenticated\)/admin/settings/software-update/+page.server.ts 2>/dev/null | head -100

Length of output: 2505



---

🏁 Script executed:

```shell
#!/bin/bash
# Step 1: View Prisma schema to understand available tables
echo "=== Viewing Prisma schema ==="
cat src/lib/prisma/schema.prisma | head -200

# Step 2: Search for ProductTransitions table
echo -e "\n=== Searching for ProductTransition references ==="
rg -n "ProductTransition" --type ts -C2

# Step 3: Find doProductAction implementation
echo -e "\n=== Locating doProductAction ==="
fd -e ts -e js | xargs grep -l "doProductAction" | head -5

# Step 4: Search for comment storage patterns more broadly
echo -e "\n=== Searching for comment storage patterns ==="
rg -n "comment" --type ts -C2 -i | grep -i "save\|store\|create\|update" | head -20

Length of output: 21393



---

🏁 Script executed:

```shell
#!/bin/bash
# Step 1: Get ProductTransitions model from Prisma schema
echo "=== ProductTransitions model in schema ==="
cat src/lib/prisma/schema.prisma | grep -A 30 "model ProductTransitions"

# Step 2: Check doProductAction implementation
echo -e "\n=== doProductAction implementation ==="
cat src/lib/products/server.ts | grep -A 40 "export.*doProductAction"

# Step 3: See how ProductTransitions are created for other actions (e.g., CancelWorkflow)
echo -e "\n=== Example: How CancelWorkflow stores transition ==="
cat src/lib/products/server.ts | grep -B 5 -A 5 "CancelWorkflow"

Length of output: 3010



---

🏁 Script executed:

```shell
#!/bin/bash
# Check ProductTransitionType enum values
echo "=== ProductTransitionType enum values ==="
cat src/lib/prisma.ts | grep -A 10 "export enum ProductTransitionType"

Length of output: 217


Implement comment persistence in ProductTransitions; decide on TransitionType enum value.

The TODO is valid. The codebase pattern (used for CancelWorkflow, etc.) stores similar actions in ProductTransitions with an optional Comment field. However, ProductTransitionType enum lacks a Rebuild or SoftwareUpdate value. For each product, create a ProductTransition entry using:

  • Existing: ProductTransitionType.Activity (generic fallback), or
  • Add: New enum value SoftwareUpdate or Rebuild to ProductTransitionType

Use DatabaseWrites.productTransitions.createMany() to batch-persist transitions with the collected form.data.comment for the rebuild operation.

🤖 Prompt for AI Agents
In src/routes/(authenticated)/admin/settings/software-update/+page.server.ts
around lines 67 to 68, the TODO notes that the rebuild comment
(form.data.comment) is not persisted; implement persistence by creating
ProductTransition records for each product using
DatabaseWrites.productTransitions.createMany(), include the comment value in the
Comment field, and set the TransitionType to a clear value — either add a new
ProductTransitionType entry (recommended: SoftwareUpdate or Rebuild) and use
that, or use the existing ProductTransitionType.Activity as a generic fallback;
ensure the enum is extended where defined and any needed migrations/types are
updated so the new enum value compiles and the batch create includes ProductId,
TransitionType, CreatedBy, optional Comment, and any other required fields.

Comment on lines 1 to 21
<script lang="ts">
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms';
import { toast } from '$lib/utils';
import { goto } from '$app/navigation';
import { getLocale, localizeHref } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
import LabeledFormInput from '$lib/components/settings/LabeledFormInput.svelte';
interface Props {
data: PageData;
}
let { data }: Props = $props();
const base = '/admin/settings/software-update';
const { form, enhance } = superForm(data.form, {
onUpdated({ form }) {
if (form.valid) {
goto(localizeHref(base));
toast('success', m.admin_software_update_toast_success());
}
}
});
let propsOk = $state(true);
</script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix formatting and remove unused imports.

Multiple formatting issues and an unused import are flagged by the linter.

Apply this diff to fix formatting and remove the unused getLocale import:

 <script lang="ts">
-    import type { PageData } from './$types';
-    import { superForm } from 'sveltekit-superforms';
-    import { toast } from '$lib/utils';
-    import { goto } from '$app/navigation';
-    import { getLocale, localizeHref } from '$lib/paraglide/runtime';
-    import { m } from '$lib/paraglide/messages';
-    import LabeledFormInput from '$lib/components/settings/LabeledFormInput.svelte';
+  import type { PageData } from './$types';
+  import { superForm } from 'sveltekit-superforms';
+  import LabeledFormInput from '$lib/components/settings/LabeledFormInput.svelte';
+  import { goto } from '$app/navigation';
+  import { localizeHref } from '$lib/paraglide/runtime';
+  import { m } from '$lib/paraglide/messages';
+  import { toast } from '$lib/utils';
 
-    interface Props {
-        data: PageData;
-    }
+  interface Props {
+    data: PageData;
+  }
 
-    let { data }: Props = $props();
-    
+  let { data }: Props = $props();
 
-    const base = '/admin/settings/software-update';
+  const base = '/admin/settings/software-update';
 
-
-    
-    const { form, enhance } = superForm(data.form, {
+  const { form, enhance } = superForm(data.form, {
     onUpdated({ form }) {
       if (form.valid) {
         goto(localizeHref(base));
         toast('success', m.admin_software_update_toast_success());
       }
     }
-    });
-    
-
+  });
 
-    let propsOk = $state(true);
+  let propsOk = $state(true);
 </script>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script lang="ts">
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms';
import { toast } from '$lib/utils';
import { goto } from '$app/navigation';
import { getLocale, localizeHref } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
import LabeledFormInput from '$lib/components/settings/LabeledFormInput.svelte';
interface Props {
data: PageData;
}
let { data }: Props = $props();
const base = '/admin/settings/software-update';
const { form, enhance } = superForm(data.form, {
onUpdated({ form }) {
if (form.valid) {
goto(localizeHref(base));
toast('success', m.admin_software_update_toast_success());
}
}
});
let propsOk = $state(true);
</script>
<script lang="ts">
import type { PageData } from './$types';
import { superForm } from 'sveltekit-superforms';
import LabeledFormInput from '$lib/components/settings/LabeledFormInput.svelte';
import { goto } from '$app/navigation';
import { localizeHref } from '$lib/paraglide/runtime';
import { m } from '$lib/paraglide/messages';
import { toast } from '$lib/utils';
interface Props {
data: PageData;
}
let { data }: Props = $props();
const base = '/admin/settings/software-update';
const { form, enhance } = superForm(data.form, {
onUpdated({ form }) {
if (form.valid) {
goto(localizeHref(base));
toast('success', m.admin_software_update_toast_success());
}
}
});
let propsOk = $state(true);
</script>
🧰 Tools
🪛 GitHub Check: checks / typecheck-lint

[failure] 11-11:
Replace ········ with ····


[failure] 10-10:
Delete ··


[failure] 8-8:
Delete ··


[warning] 7-7:
$lib/paraglide/messages import should occur after import of $lib/components/settings/LabeledFormInput.svelte


[failure] 7-7:
Delete ··


[warning] 6-6:
'getLocale' is defined but never used


[warning] 6-6:
$lib/paraglide/runtime import should occur after import of $lib/components/settings/LabeledFormInput.svelte


[failure] 6-6:
Delete ··


[failure] 5-5:
Delete ··


[warning] 4-4:
$lib/utils import should occur after import of $lib/components/settings/LabeledFormInput.svelte


[failure] 4-4:
Replace ···· with ··


[failure] 3-3:
Delete ··


[warning] 2-2:
./$types type import should occur after import of sveltekit-superforms


[failure] 2-2:
Delete ··

🤖 Prompt for AI Agents
In src/routes/(authenticated)/admin/settings/software-update/+page.svelte around
lines 1 to 33, remove the unused getLocale import from the import list and clean
up formatting/whitespace (align imports, remove extra blank lines, and normalize
indentation) so the file adheres to the linter rules; keep the rest of the logic
intact (retain localizeHref, superForm usage, toast, goto, interface and props
handling) and ensure there are no remaining unused imports or extraneous blank
lines.

let propsOk = $state(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The propsOk state is always true and appears incomplete.

The propsOk variable is hardcoded to true, making the submit button's disabled attribute ineffective. This suggests incomplete implementation.

If form validation is needed, consider deriving propsOk from the form state:

-  let propsOk = $state(true);
+  let propsOk = $derived($form.comment?.trim().length > 0);

Alternatively, if no additional validation is needed beyond the form's built-in validation, remove the disabled attribute entirely:

-  <input type="submit" class="btn btn-primary" value={m.admin_software_update_rebuild_start()} disabled={!propsOk}/>
+  <input type="submit" class="btn btn-primary" value={m.admin_software_update_rebuild_start()} />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let propsOk = $state(true);
let propsOk = $derived($form.comment?.trim().length > 0);
🤖 Prompt for AI Agents
In src/routes/(authenticated)/admin/settings/software-update/+page.svelte around
line 32, the propsOk variable is hardcoded to true which makes the submit
button's disabled attribute ineffective; replace this placeholder with a real
derivation (e.g., derive propsOk from the form state or element validity using a
reactive statement that calls formEl?.checkValidity() or a Svelte store
reflecting input validity) or remove the disabled binding entirely if you rely
solely on native HTML validation; update any references to propsOk accordingly
and ensure the form element reference or store is initialized so the reactive
validity check works.

Comment on lines 40 to 42
<LabeledFormInput key="admin_nav_software_update_comment">
<input
type="text"
name="comment"
class="input input-bordered w-full validator"
bind:value={$form.comment}
required
/>
<span class="validator-hint">{m.admin_software_update_comment_required()}</span>
</LabeledFormInput>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistency: form schema allows nullable comment but UI marks it required.

The server-side schema defines comment as nullable (v.nullable(v.string())), but the UI input has the required attribute and shows a "required" validation message. This creates confusion about whether the comment is actually required.

Choose one approach:

Option 1: If the comment should be required, update the server schema:

In +page.server.ts:

 const formSchema = v.object({
-  comment: v.nullable(v.string())
+  comment: v.pipe(v.string(), v.minLength(1, 'Comment is required'))
 });

Option 2: If the comment should be optional, update the UI:

     <input
       type="text"
       name="comment"
       class="input input-bordered w-full validator"
       bind:value={$form.comment}
-      required
     />
-    <span class="validator-hint">{m.admin_software_update_comment_required()}</span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<LabeledFormInput key="admin_nav_software_update_comment">
<input
type="text"
name="comment"
class="input input-bordered w-full validator"
bind:value={$form.comment}
required
/>
<span class="validator-hint">{m.admin_software_update_comment_required()}</span>
</LabeledFormInput>
<LabeledFormInput key="admin_nav_software_update_comment">
<input
type="text"
name="comment"
class="input input-bordered w-full validator"
bind:value={$form.comment}
/>
</LabeledFormInput>

Copy link
Contributor

@FyreByrd FyreByrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As best as I can tell, all of the CodeRabbit stuff this time around is accurate; please address those.

Moving forward, please make sure you are running npm run check and npm run format and fix all reported errors before pushing.

There are some potential changes for the software updates page that @chrisvire and I had discussed briefly earlier last week; we will need to discuss those more in depth and update the issues accordingly. One of us will let you know when that happens.


//// Get all products
// Get what organizations the admin is in
const orgIds: number[] = locals.security.organizationMemberships;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user is a super admin, this should be for all orgs, not just whatever orgs the user is in.
Also, it is possible for a user to be a member of an org, but not necessarily an admin for that org. See #1368 and its associated PR (#1370) for more details.

let propsOk = $state(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is not modified anywhere?

);

// TODO: The comment still needs to be stored somewhere.
// The comment value is at form.data.comment
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment will ultimately go on the associated ProductTransitions entry (Start Rebuild Workflow [TransitionType = StartWorkflow, WorkflowType = Rebuild]) for each product that is rebuilt. You will need to either do it in or right after this await Promise.all call, or, even better, add comment as an optional parameter for doProductAction and Workflow.create and then write it to the db in this part of Workflow.create

await DatabaseWrites.productTransitions.create({
    data: {
      ProductId: productId,
      DateTransition: new Date(),
      TransitionType: ProductTransitionType.StartWorkflow,
      WorkflowType: config.workflowType
      // Comment here?
    }
  });

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe comment is already an optional parameter according to the Schema, working on it now

<span class="validator-hint">{m.admin_software_update_comment_required()}</span>
</LabeledFormInput>
<input type="submit" class="btn btn-primary" value={m.admin_software_update_rebuild_start()} disabled={!propsOk}/>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would probably be helpful to show a list of Projects/Products that would be affected.

@ThePitter
Copy link
Author

See Comment in #1249 For things that need to be changed before making this PR complete again.

@ThePitter ThePitter force-pushed the feature/software-update-rebuild branch from 97f04df to 613c8b0 Compare November 20, 2025 18:13
@ThePitter ThePitter marked this pull request as ready for review November 26, 2025 22:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (3)

15-36: Consider extracting duplicate organization resolution logic.

The organization resolution logic (lines 16-36) is nearly identical to the logic in the start action (lines 106-126). Extracting this into a shared helper function would reduce duplication and maintenance burden.

async function resolveTargetOrgs(
  params: { orgId?: string },
  locals: App.Locals
): Promise<number[]> {
  const organizationId = Number(params.orgId);
  if (!isNaN(organizationId)) {
    locals.security.requireAdminOfOrg(organizationId);
    return [organizationId];
  }
  
  const orgs = await DatabaseReads.userRoles.findMany({
    where: {
      UserId: locals.security.userId,
      RoleId: { in: [1, 2] }
    },
    select: { OrganizationId: true }
  });
  
  const searchOrgs = new Set<number>();
  for (const org of orgs) {
    locals.security.requireAdminOfOrg(org.OrganizationId);
    searchOrgs.add(org.OrganizationId);
  }
  return Array.from(searchOrgs);
}

Also applies to: 106-126


47-50: Prefer map over forEach with push.

-  const organizationsReadable: string[] = [];
-  names.forEach((name) => {
-    organizationsReadable.push(name.Name ? name.Name : 'Unknown Organization');
-  });
+  const organizationsReadable = names.map((n) => n.Name ?? 'Unknown Organization');

156-162: Consider using Promise.allSettled for resilience.

If one doProductAction fails, Promise.all will reject immediately and no results are returned. Using Promise.allSettled would allow partial success and provide feedback about which products failed.

-    await Promise.all(
-      projects.flatMap((project) =>
-        project.Products.map((p) =>
-          doProductAction(p.Id, ProductActionType.Rebuild, form.data.comment)
-        )
-      )
-    );
+    const results = await Promise.allSettled(
+      projects.flatMap((project) =>
+        project.Products.map((p) =>
+          doProductAction(p.Id, ProductActionType.Rebuild, form.data.comment)
+        )
+      )
+    );
+    
+    const failed = results.filter((r) => r.status === 'rejected');
+    if (failed.length > 0) {
+      console.error(`${failed.length} product rebuild(s) failed`);
+      // Optionally return partial failure info to the client
+    }

     return { ok: true, form };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80d6465 and 9d45141.

📒 Files selected for processing (8)
  • src/lib/locales/en-US.json (1 hunks)
  • src/lib/locales/es-419.json (1 hunks)
  • src/lib/locales/fr-FR.json (1 hunks)
  • src/lib/products/server.ts (3 hunks)
  • src/lib/server/workflow/index.ts (2 hunks)
  • src/routes/(authenticated)/+layout.svelte (1 hunks)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (1 hunks)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/locales/es-419.json
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T14:07:02.200Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:285-303
Timestamp: 2025-09-12T14:07:02.200Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to optimize filterAvailableActions by creating Sets at the caller level rather than inside the function, so the function would take Set<number> arguments instead of arrays for better performance and separation of concerns.

Applied to files:

  • src/lib/products/server.ts
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-22T14:59:53.366Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts:214-221
Timestamp: 2025-09-22T14:59:53.366Z
Learning: In the AppBuilder portal, bulk project actions in the route `/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts` are organization-scoped by design. The route parameter `[orgId=idNumber]` ensures all operations happen within a single organization context, so using the route-level orgId for all projects in bulk actions like `doProjectAction` is correct and safe.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-12T14:57:08.647Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:252-264
Timestamp: 2025-09-12T14:57:08.647Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to use transition.target[0] prediction mechanism for determining post-send state rather than calling flow.state() after flow.send() due to race condition concerns. The prediction approach is sufficient for redirect decisions between staying on task page vs going to projects page.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
🧬 Code graph analysis (1)
src/lib/server/workflow/index.ts (1)
src/lib/workflowTypes.ts (1)
  • WorkflowConfig (180-184)
🪛 GitHub Check: checks / typecheck-lint
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts

[warning] 53-53:
'projects' is assigned a value but never used

🔇 Additional comments (5)
src/lib/products/server.ts (1)

7-11: LGTM!

The optional comment parameter is correctly added to doProductAction and appropriately propagated to both Workflow.create for Rebuild/Republish actions and persisted in ProductTransitions for Cancel actions.

Also applies to: 53-61, 87-87

src/lib/server/workflow/index.ts (1)

51-55: LGTM!

The Workflow.create method signature correctly accepts an optional comment parameter and persists it in the StartWorkflow transition record. This maintains backward compatibility with existing callers.

Also applies to: 89-97

src/routes/(authenticated)/+layout.svelte (1)

220-229: LGTM!

The new navigation item follows the established pattern for admin links, correctly uses activeOrgUrl for organization-scoped navigation, and is appropriately gated by the admin role check.

src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte (1)

18-25: Verify post-submission navigation preserves org context.

After a successful form submission, the navigation goes to /software-update without preserving the current orgId from the URL. If the user initiated the rebuild for a specific organization, they'll be redirected to the "all organizations" view.

Is this intentional, or should the navigation preserve the current organization context? If preservation is desired:

+  import { page } from '$app/state';
+
   const { form, enhance } = superForm(data.form, {
     onUpdated({ form }) {
       if (form.valid) {
-        goto(localizeHref(base));
+        goto(localizeHref(page.url.pathname));
         toast('success', m.admin_software_update_toast_success());
       }
     }
   });
src/lib/locales/en-US.json (1)

559-565: LGTM!

The new localization keys follow the established naming conventions and provide clear, user-friendly messages for the software update feature.

<div class="m-4">
<p class="pl-4">{m.admin_nav_software_update_description()}</p>
<br />
<p class="pl-4"><b>This will affect the following organizations: {data.organizations}</b></p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded English text should be localized.

The string "This will affect the following organizations: " is hardcoded in English. For consistency with the rest of the application's i18n approach, this should use a localization key.

-    <p class="pl-4"><b>This will affect the following organizations: {data.organizations}</b></p>
+    <p class="pl-4"><b>{m.admin_software_update_affected_orgs({ organizations: data.organizations })}</b></p>

You'll need to add the corresponding key to each locale file (e.g., "admin_software_update_affected_orgs": "This will affect the following organizations: {organizations}").

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte
around line 33, the sentence "This will affect the following organizations:
{data.organizations}" is hardcoded English; replace it with a localized string
reference (e.g., use your svelte i18n helper or
$t('admin_software_update_affected_orgs', { organizations: data.organizations
})) so the organizations value is interpolated into the translated text, and add
the key "admin_software_update_affected_orgs": "This will affect the following
organizations: {organizations}" (and corresponding translations) to each locale
file.

@becca-perk becca-perk requested a review from FyreByrd December 2, 2025 01:47
Copy link
Contributor

@FyreByrd FyreByrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is starting to look better.
A few things for now:

  • There is some duplicated/incorrect/unnecessary logic when getting a list of organizations on the server side. Please take a look at src/routes/users/org/[[id=idNumber]]/+page.server.ts for inspiration.
  • We recently added a SystemVersions table with information on AppBuilder versions. Please use this to further filter projects.
  • Both the load function and the action should have the same filtration logic for orgs/projects/products. Please break this out into one or more functions (in the same file) for consistency/reusability.

I will be working on creating a database table to record software updates for you to write to/read from for this PR. I will ping one of you when that's ready.

});

// Check for rebuild status...
const projects = await DatabaseReads.projects.findMany({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably also be returned in the load function so you can start displaying a list of affected projects.

where: {
OrganizationId: searchOrgs ? { in: searchOrgs } : undefined,
DateArchived: null,
RebuildOnSoftwareUpdate: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can have nested filters here to filter based on most recently built appbuilder version in the product builds.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is already happening at line 68, unless you are referencing a different way?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I am suggesting is to filter projects based on having available products. What is currently happening is getting all relevant projects, with no regard to available products, then for each project getting all available products, meaning some projects will be returned with an empty array of products.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FyreByrd is there a specific way to determine what is the 'current' build version? I see us storing what the current version of the application is but not what version number they 'should' be if out of date.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing this may be rather complicated....

So, for what is:
ProductBuild.AppBuilderVersion
Project.ApplicationTypeId
Organization.BuildEngineUrl (or env.DEFAULT_BUILD_ENGINE_URL if Organization.UseDefaultBuildEngine)

For what should be:
SystemVersions.*

If what is does not match what should be, it is out of date.
Hope this helps?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will use ApplicationTypeId and BuildEngineUrl as the key into SystemVersions, and compare Version against AppBuilderVersion

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, tracking now. Will fix that and get it referencing the correct thing

// Determine what organizations are being affected
const organizationId = Number(params.orgId);
let searchOrgs: number[] = [];
if (isNaN(organizationId)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments as in above load function. I would recommend separating this common functionality out into a separate function so the logic is consistent and easily modifiable. Also, params.orgId will always be either a number or not present at all (i.e. undefined).

include: {
Products: {
where: {
WorkflowInstance: null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure on this, but products may need to be filtered a bit further?

<div class="m-4">
<p class="pl-4">{m.admin_nav_software_update_description()}</p>
<br />
<p class="pl-4"><b>This will affect the following organizations: {data.organizations}</b></p>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be added to the locale JSONs

@FyreByrd
Copy link
Contributor

FyreByrd commented Dec 4, 2025

Just opened #1400 to add a table you can write to/display from for in progress software updates.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (1)

24-41: Super-admin branch in determineTargetOrgs doesn’t truly return “all organizations”.

The docstring says super admins get all organizations, but the implementation reads from userRoles:

const orgs = locals.security.isSuperAdmin
  ? await DatabaseReads.userRoles.findMany({ select: { OrganizationId: true } })
  : await DatabaseReads.userRoles.findMany({
    where: { UserId: locals.security.userId, RoleId: { in: [RoleId.SuperAdmin, RoleId.OrgAdmin] } },
    select: { OrganizationId: true }
  });

This only returns orgs that have at least one userRoles row, and for non-super-admins, only those where the current user holds specific roles. That can diverge from the stated behavior and prior feedback that super admins should be able to target all orgs, independent of their memberships.

A clearer and more accurate approach would be something like:

async function determineTargetOrgs(locals: App.Locals): Promise<number[]> {
  if (locals.security.isSuperAdmin) {
    const orgs = await DatabaseReads.organizations.findMany({
      select: { Id: true }
    });
    return orgs.map((o) => o.Id);
  }

  const roles = await DatabaseReads.userRoles.findMany({
    where: {
      UserId: locals.security.userId,
      RoleId: { in: [RoleId.SuperAdmin, RoleId.OrgAdmin] }
    },
    select: { OrganizationId: true }
  });

  return Array.from(new Set(roles.map((r) => r.OrganizationId)));
}

This matches the docstring and ensures super admins truly cover all organizations, while non-super-admins are constrained to orgs where they’re actually admins. Based on learnings, this aligns better with FyreByrd’s expectation that super admins see all orgs, not just those where they have explicit org-role rows.

🧹 Nitpick comments (3)
src/lib/locales/es-419.json (1)

559-566: Software-update strings look good; consider localizing downloads_title.

The new admin_*software_update* keys read well and align with the described behavior. One minor nit: downloads_title is still "Downloads" in the es-419 locale—if this is user-facing, you may want to translate it (e.g., "Descargas") for consistency with the rest of the file.

src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (2)

11-14: Whitespace-only comments still pass validation; consider trimming input.

v.minLength(1, 'Comment is required') only enforces length, so a comment like " " will be accepted even though it’s effectively empty. If you truly require a meaningful comment, consider trimming before checking length, e.g.:

-const formSchema = v.object({
-  comment: v.pipe(v.string(), v.minLength(1, 'Comment is required'))
-  // Since we are only getting a comment, I do not believe we need a properties: propertiesSchema here.
-});
+const formSchema = v.object({
+  comment: v.pipe(
+    v.string(),
+    v.trim(),
+    v.minLength(1, 'Comment is required')
+  )
+  // Since we are only getting a comment, I do not believe we need a properties: propertiesSchema here.
+});

Please double-check that v.trim() (or the equivalent in your valibot version) is available and behaves as expected, or apply an equivalent trim via a transform/refinement.


123-165: SystemVersions usage is simplified to “latest row”; may need per-appType/buildEngine logic.

Right now, the action treats the most recent systemVersions row as a single global AppBuilder version:

const systemVersion = await DatabaseReads.systemVersions.findFirst({
  orderBy: { DateCreated: 'desc' }
});
const currentAppBuilderVersion = systemVersion?.Version ?? null;

and then excludes products already at that version via:

NOT: [
  { VersionBuilt: currentAppBuilderVersion },
  { ProductBuilds: { some: { Version: currentAppBuilderVersion } } }
]

From earlier guidance, SystemVersions.* is intended to be keyed by Project.ApplicationTypeId and the effective BuildEngineUrl, and “what should be” is looked up per (applicationType, buildEngine) combination. If there are multiple rows for different combinations, using just the single latest row may misclassify some products (either rebuilding ones that are already up to date for their specific combo or vice versa).

Consider evolving this to:

  • Look up the appropriate “target” SystemVersions row per project (keyed by ApplicationTypeId and effective BuildEngineUrl), and
  • Filter products per project against that target version.

This will be more complex (likely requiring either a pre-fetched map of SystemVersions or additional queries), but it will more closely match the intended “out-of-date relative to its own AppBuilder/engine combo” semantics. Based on learnings, this would align with the earlier what is vs what should be design for SystemVersions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d45141 and eeb56c6.

📒 Files selected for processing (5)
  • src/lib/locales/en-US.json (1 hunks)
  • src/lib/locales/es-419.json (1 hunks)
  • src/lib/locales/fr-FR.json (1 hunks)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (1 hunks)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte
  • src/lib/locales/fr-FR.json
  • src/lib/locales/en-US.json
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-09-12T14:07:02.200Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:285-303
Timestamp: 2025-09-12T14:07:02.200Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to optimize filterAvailableActions by creating Sets at the caller level rather than inside the function, so the function would take Set<number> arguments instead of arrays for better performance and separation of concerns.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-22T14:59:53.366Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts:214-221
Timestamp: 2025-09-22T14:59:53.366Z
Learning: In the AppBuilder portal, bulk project actions in the route `/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts` are organization-scoped by design. The route parameter `[orgId=idNumber]` ensures all operations happen within a single organization context, so using the route-level orgId for all projects in bulk actions like `doProjectAction` is correct and safe.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-12T14:57:08.647Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:252-264
Timestamp: 2025-09-12T14:57:08.647Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to use transition.target[0] prediction mechanism for determining post-send state rather than calling flow.state() after flow.send() due to race condition concerns. The prediction approach is sufficient for redirect decisions between staying on task page vs going to projects page.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-12T14:31:23.906Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/lib/server/workflow/index.ts:52-74
Timestamp: 2025-09-12T14:31:23.906Z
Learning: In the appbuilder-portal codebase, optional chaining in database queries (like `check?.Project._count.Authors`) is primarily used to satisfy TypeScript's type checker rather than handling expected null cases. The `check` being null should realistically never happen in the normal application flow.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-22T15:00:43.129Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.server.ts:7-10
Timestamp: 2025-09-22T15:00:43.129Z
Learning: In user settings layouts, use the `adminOrgs` function from `$lib/users/server` to determine admin access rather than throwing 403 errors upfront. The `adminOrgs` function creates a filter that returns organizations where the subject user is a member AND the current user is an admin of that organization.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-25T19:44:14.845Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/import/[id=idNumber]/+page.server.ts:24-26
Timestamp: 2025-09-25T19:44:14.845Z
Learning: The `requireHasRole(organizationId, roleId, orOrgAdmin)` method in the Security class allows organization admins to pass role checks when the third parameter `orOrgAdmin` is set to `true`, even if they don't have the specific role being checked for.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-17T21:07:18.139Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/lib/projects/index.ts:183-201
Timestamp: 2025-09-17T21:07:18.139Z
Learning: In the AppBuilder portal project, org admins must be in the project group to claim/take ownership of projects. This is enforced both in client-side logic (canClaimProject function) and server-side validation.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-10-01T15:58:37.578Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1317
File: src/routes/(authenticated)/users/+page.svelte:51-55
Timestamp: 2025-10-01T15:58:37.578Z
Learning: In the appbuilder-portal codebase, the $orgActive store (from $lib/stores) is designed to always contain a valid organization ID and should never be null. Code that syncs values to $orgActive should include guard clauses to prevent null assignments.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-10T13:46:43.497Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/auth.ts:240-245
Timestamp: 2025-09-10T13:46:43.497Z
Learning: In src/auth.ts, the products branch intentionally uses params.id as a string for database lookups, not as a number. This is the correct behavior and should not be changed to numeric parsing.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
🧬 Code graph analysis (1)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (2)
src/lib/server/database/prisma.ts (1)
  • DatabaseReads (22-22)
src/lib/products/server.ts (1)
  • doProductAction (7-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks / build-and-test
🔇 Additional comments (1)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (1)

129-175: Rebuild selection + triggering logic looks consistent with the intended behavior.

The projects query and the subsequent Promise.all over projects.flatMap(project => project.Products) correctly:

  • Limit to non-archived projects with RebuildOnSoftwareUpdate: true in the target orgs.
  • Exclude products with an active WorkflowInstance.
  • Exclude products already at the chosen “current” AppBuilder version (when defined).
  • Pass the validated comment through to doProductAction with ProductActionType.Rebuild.

This wiring matches the described software-update semantics and cleanly reuses the existing product action pipeline.

Copy link
Contributor

@FyreByrd FyreByrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments below for my feedback on your most recent changes.

I am looking forward to seeing what y'all come up with for UI. If you need ideas I can send a DM of a rough sketch I had in mind.

export const load = (async ({ url, locals, params }) => {
// Determine what organizations are being affected
const searchOrgs = await getOrganizations(locals, params);
if (isNaN(Number(params.orgId))) locals.security.requireAdminOfOrgIn(searchOrgs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to check if param.orgId is NaN.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain if you're referencing this as a 'you can just convert to number and have the same functionality' or if you're saying I don't need the if statement at all. The check itself does not appear to have any measurable performance penalty and personally I think it reads better. If you're referencing that I don't need to perform the check at all, I run into issues with lint not liking not having security checks (even if implicitly by logic you wouldn't load anything anyways.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You still need the security checks. All I'm saying is that it's not necessary to check if the organization id does or does not parse to a valid number. You just need to check if it is present.

: await DatabaseReads.userRoles.findMany({
where: {
UserId: locals.security.userId,
RoleId: { in: [RoleId.SuperAdmin, RoleId.OrgAdmin] } // Must be a super admin or organization admin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're already checking for Super Admin above, so you shouldn't be filtering for that here.

},
select: { OrganizationId: true }
});
const searchOrgs = new Set<number>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the attention to detail, but ensuring uniqueness really isn't necessary, especially if you follow my above suggestions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to ensure uniqueness for display purposes, you can do this on the client side instead? I would also recommend just passing the array of ids directly into the set constructor.

});
const organizationsReadable = names.map((n) => n.Name ?? 'Unknown Organization');

// TODO: @becca-perk? Use information from most recent product transition to determine whether to show 'start' or 'pause' on button and action being called.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't get this information from the ProductTransitions table. You should be able to get this just fine from the new SoftwareUpdates table.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a minute to realize you were referring to the TODO, whoops. Will make a note @becca-perk

...(currentAppBuilderVersion
? {
NOT: [
{ VersionBuilt: currentAppBuilderVersion },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VersionBuilt is not what you are wanting to query on. This is the version of the app, not the version of AppBuilders used to build it.

Copy link
Author

@ThePitter ThePitter Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow missed that you'd replied to some of my earlier comments on where I should pull versioning from, fixing now.

? {
NOT: [
{ VersionBuilt: currentAppBuilderVersion },
{ ProductBuilds: { some: { Version: currentAppBuilderVersion } } }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version is the version of the app, not the version of AppBuilders used to build it.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (3)

28-34: Remove redundant RoleId.SuperAdmin from query filter.

The query includes RoleId.SuperAdmin in the filter, but any user who is a SuperAdmin has already returned on lines 21-26. The query will never match SuperAdmin roles at this point.

Apply this diff:

   const roles = await DatabaseReads.userRoles.findMany({
     where: {
       UserId: locals.security.userId,
-      RoleId: { in: [RoleId.SuperAdmin, RoleId.OrgAdmin] }
+      RoleId: RoleId.OrgAdmin
     },
     select: { OrganizationId: true }
   });

59-59: Complete the JSDoc comment.

The comment is incomplete: "Array of Pro"

Apply this diff:

- * @returns Array of Pro
+ * @returns Array of products that need to be rebuilt

73-77: Fix comment or implementation mismatch.

Line 73 comment states "We need this to identify the latest build, assuming WorkflowBuildId is monotonically increasing", but line 77 orders by Id: 'desc' instead of WorkflowBuildId. Either update the comment to reflect ordering by Id, or order by WorkflowBuildId if that's the intended behavior.

If ordering by WorkflowBuildId is correct:

       ProductBuilds: {
-        orderBy: { Id: 'desc' }, // Order by ID descending to get the 'latest' build
+        orderBy: { WorkflowBuildId: 'desc' }, // Order by WorkflowBuildId descending to get the 'latest' build
         take: 1, // Only take the most recent

Otherwise, update the comment to match the implementation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eeb56c6 and 3ef32b3.

📒 Files selected for processing (2)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (1 hunks)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-09-12T14:07:02.200Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:285-303
Timestamp: 2025-09-12T14:07:02.200Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to optimize filterAvailableActions by creating Sets at the caller level rather than inside the function, so the function would take Set<number> arguments instead of arrays for better performance and separation of concerns.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-22T14:59:53.366Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts:214-221
Timestamp: 2025-09-22T14:59:53.366Z
Learning: In the AppBuilder portal, bulk project actions in the route `/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts` are organization-scoped by design. The route parameter `[orgId=idNumber]` ensures all operations happen within a single organization context, so using the route-level orgId for all projects in bulk actions like `doProjectAction` is correct and safe.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-12T14:57:08.647Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:252-264
Timestamp: 2025-09-12T14:57:08.647Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to use transition.target[0] prediction mechanism for determining post-send state rather than calling flow.state() after flow.send() due to race condition concerns. The prediction approach is sufficient for redirect decisions between staying on task page vs going to projects page.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-12T14:31:23.906Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/lib/server/workflow/index.ts:52-74
Timestamp: 2025-09-12T14:31:23.906Z
Learning: In the appbuilder-portal codebase, optional chaining in database queries (like `check?.Project._count.Authors`) is primarily used to satisfy TypeScript's type checker rather than handling expected null cases. The `check` being null should realistically never happen in the normal application flow.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-22T15:00:43.129Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.server.ts:7-10
Timestamp: 2025-09-22T15:00:43.129Z
Learning: In user settings layouts, use the `adminOrgs` function from `$lib/users/server` to determine admin access rather than throwing 403 errors upfront. The `adminOrgs` function creates a filter that returns organizations where the subject user is a member AND the current user is an admin of that organization.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-25T19:44:14.845Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/import/[id=idNumber]/+page.server.ts:24-26
Timestamp: 2025-09-25T19:44:14.845Z
Learning: The `requireHasRole(organizationId, roleId, orOrgAdmin)` method in the Security class allows organization admins to pass role checks when the third parameter `orOrgAdmin` is set to `true`, even if they don't have the specific role being checked for.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-17T21:07:18.139Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/lib/projects/index.ts:183-201
Timestamp: 2025-09-17T21:07:18.139Z
Learning: In the AppBuilder portal project, org admins must be in the project group to claim/take ownership of projects. This is enforced both in client-side logic (canClaimProject function) and server-side validation.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-10-01T15:58:37.578Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1317
File: src/routes/(authenticated)/users/+page.svelte:51-55
Timestamp: 2025-10-01T15:58:37.578Z
Learning: In the appbuilder-portal codebase, the $orgActive store (from $lib/stores) is designed to always contain a valid organization ID and should never be null. Code that syncs values to $orgActive should include guard clauses to prevent null assignments.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-10T13:46:43.497Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/auth.ts:240-245
Timestamp: 2025-09-10T13:46:43.497Z
Learning: In src/auth.ts, the products branch intentionally uses params.id as a string for database lookups, not as a number. This is the correct behavior and should not be changed to numeric parsing.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-10-02T01:36:08.124Z
Learnt from: 7dev7urandom
Repo: sillsdev/appbuilder-portal PR: 1318
File: src/lib/components/OrganizationDropdown.svelte:30-39
Timestamp: 2025-10-02T01:36:08.124Z
Learning: In the `src/lib/components/OrganizationDropdown.svelte` component, the `organizations` array will always contain at least one organization, so there's no need to handle the empty array case.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
🧬 Code graph analysis (1)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (2)
src/lib/server/database/prisma.ts (1)
  • DatabaseReads (22-22)
src/lib/products/server.ts (1)
  • doProductAction (7-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks / build-and-test
🔇 Additional comments (2)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (2)

179-183: Consider error handling for partial rebuild failures.

If any doProductAction call fails during Promise.all, the entire operation fails immediately and previously triggered rebuilds cannot be rolled back. Consider whether this is the desired behavior or if you need:

  • Individual error handling with Promise.allSettled to track which rebuilds succeeded/failed
  • Returning partial success information to the user
  • Transaction-like behavior or compensation logic

Should partial rebuild failures be handled gracefully? If so, consider using Promise.allSettled:

-    await Promise.all(
-      productsToRebuild.map((p) =>
-        doProductAction(p.id, ProductActionType.Rebuild, form.data.comment)
-      )
-    );
-
-    return { form, ok: true };
+    const results = await Promise.allSettled(
+      productsToRebuild.map((p) =>
+        doProductAction(p.id, ProductActionType.Rebuild, form.data.comment)
+      )
+    );
+
+    const failures = results.filter((r) => r.status === 'rejected');
+    if (failures.length > 0) {
+      // Handle partial failures - return info about what succeeded/failed
+      return { 
+        form, 
+        ok: false, 
+        partial: true,
+        failedCount: failures.length,
+        totalCount: results.length 
+      };
+    }
+
+    return { form, ok: true };

107-107: Verify BuildEngineUrl fallback behavior.

When BuildEngineUrl is null, the code uses an empty string. Should this use the DEFAULT_BUILD_ENGINE_URL environment variable instead, consistent with other parts of the codebase?

Comment on lines +140 to +141
if (Number(params.orgId)) locals.security.requireAdminOfOrgIn(searchOrgs);
else locals.security.requireAdminOfOrg(Number(params.orgId));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix inverted security check logic.

The security check logic is backwards:

  • Line 140: When params.orgId exists (truthy), it checks requireAdminOfOrgIn(searchOrgs)
  • Line 141: When params.orgId is undefined (falsy), it tries requireAdminOfOrg(NaN) which will fail

This is the opposite of the correct logic in the start action (lines 174-175).

Apply this diff:

   // Determine what organizations are being affected
   const searchOrgs = await getOrganizations(locals, params);
-  if (Number(params.orgId)) locals.security.requireAdminOfOrgIn(searchOrgs);
-  else locals.security.requireAdminOfOrg(Number(params.orgId));
+  if (isNaN(Number(params.orgId))) locals.security.requireAdminOfOrgIn(searchOrgs);
+  else locals.security.requireAdminOfOrg(Number(params.orgId));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Number(params.orgId)) locals.security.requireAdminOfOrgIn(searchOrgs);
else locals.security.requireAdminOfOrg(Number(params.orgId));
// Determine what organizations are being affected
const searchOrgs = await getOrganizations(locals, params);
if (isNaN(Number(params.orgId))) locals.security.requireAdminOfOrgIn(searchOrgs);
else locals.security.requireAdminOfOrg(Number(params.orgId));
🤖 Prompt for AI Agents
In src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
around lines 140-141 the admin-check is inverted; when params.orgId is truthy it
currently calls requireAdminOfOrgIn(searchOrgs) and when falsy it calls
requireAdminOfOrg(Number(params.orgId)) which yields NaN. Swap the branches so
that when params.orgId exists you call
locals.security.requireAdminOfOrg(Number(params.orgId)) (convert to Number only
when present) and when it does not exist call
locals.security.requireAdminOfOrgIn(searchOrgs).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
deployment/ci/docker-compose.yml (1)

5-5: Quoting changes appear unrelated to feature scope.

The PR objective is to implement Software Update Rebuild Code with admin UI and workflow features, but this file only contains cosmetic YAML string quoting normalization (double quotes → single quotes). While the formatting is valid and consistent, these changes seem tangential to the feature.

Consider consolidating formatting changes into a separate PR to keep the commit history focused and easier to review for the feature implementation.

Also applies to: 20-20, 22-34

src/lib/server/email-service/locales/fr-FR.json (1)

77-77: Fix spacing inconsistency in the subject line.

Line 77 uses "Scriptoria :" (with space after colon), while all other entries in the subject section (e.g., lines 48–76) consistently use "Scriptoria:" (no space). Align this with the existing pattern.

-      "autoPublishOnRebuildCompleted": "Scriptoria : Publication automatique réussie de {{productName}} dans {{projectName}}",
+      "autoPublishOnRebuildCompleted": "Scriptoria: Publication automatique réussie de {{productName}} dans {{projectName}}",
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ef32b3 and e9e57eb.

📒 Files selected for processing (22)
  • .codeclimate.yml (2 hunks)
  • .vscode/settings.json (1 hunks)
  • README.md (7 hunks)
  • deployment/ci/docker-compose.yml (2 hunks)
  • deployment/development/docker-compose.yml (5 hunks)
  • eslint-rules/tsconfig.json (1 hunks)
  • renovate.json (2 hunks)
  • src/lib/otel/development_config.yml (1 hunks)
  • src/lib/otel/production_config.yml (2 hunks)
  • src/lib/products/server.ts (3 hunks)
  • src/lib/server/bullmq/pause.ts (1 hunks)
  • src/lib/server/bullmq/types.ts (4 hunks)
  • src/lib/server/email-service/locales/en-us.json (2 hunks)
  • src/lib/server/email-service/locales/es-419.json (2 hunks)
  • src/lib/server/email-service/locales/fr-FR.json (2 hunks)
  • src/lib/server/job-executors/product.ts (1 hunks)
  • src/lib/server/job-executors/system.ts (1 hunks)
  • src/lib/server/workflow/dbProcedures.ts (2 hunks)
  • src/lib/server/workflow/index.ts (6 hunks)
  • src/lib/server/workflow/state-machine.ts (6 hunks)
  • src/lib/workflowTypes.ts (4 hunks)
  • src/routes/(authenticated)/+layout.svelte (1 hunks)
✅ Files skipped from review due to trivial changes (7)
  • renovate.json
  • src/lib/otel/development_config.yml
  • .vscode/settings.json
  • eslint-rules/tsconfig.json
  • deployment/development/docker-compose.yml
  • README.md
  • .codeclimate.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/routes/(authenticated)/+layout.svelte
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-09-12T14:07:02.200Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:285-303
Timestamp: 2025-09-12T14:07:02.200Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to optimize filterAvailableActions by creating Sets at the caller level rather than inside the function, so the function would take Set<number> arguments instead of arrays for better performance and separation of concerns.

Applied to files:

  • src/lib/products/server.ts
📚 Learning: 2025-12-04T23:01:23.804Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1384
File: src/lib/server/workflow/index.ts:92-103
Timestamp: 2025-12-04T23:01:23.804Z
Learning: In src/lib/server/workflow/index.ts, the Workflow.create method intentionally creates a snapshot before creating product transitions (around line 83) and another after starting the flow (around line 103). The first snapshot is required to prevent errors when the initial state is Product Build (as in Rebuild workflows). The second snapshot ensures the user tasks job has up-to-date information after transitions are created and the flow is started.

Applied to files:

  • src/lib/products/server.ts
  • src/lib/server/workflow/index.ts
  • src/lib/workflowTypes.ts
  • src/lib/server/job-executors/product.ts
  • src/lib/server/workflow/state-machine.ts
📚 Learning: 2025-09-04T14:26:59.326Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1227
File: src/lib/server/job-executors/product.ts:247-250
Timestamp: 2025-09-04T14:26:59.326Z
Learning: In src/lib/server/job-executors/product.ts, the createLocal function's catch block returns false instead of rethrowing errors. This was implemented intentionally to fix another issue, so any changes to this error handling should be carefully evaluated for downstream impacts.

Applied to files:

  • src/lib/products/server.ts
  • src/lib/server/job-executors/product.ts
📚 Learning: 2025-08-04T18:34:58.818Z
Learnt from: 7dev7urandom
Repo: sillsdev/appbuilder-portal PR: 1186
File: src/hooks.server.ts:34-41
Timestamp: 2025-08-04T18:34:58.818Z
Learning: In the AppBuilder Portal codebase, BullMQ workers are only initialized when not building (`if (!building)` in BullWorker constructor), and the graceful shutdown handler in hooks.server.ts also only runs when not building. Therefore, the worker.worker property is guaranteed to be defined in the shutdown context, making optional chaining defensive rather than necessary.

Applied to files:

  • src/lib/server/bullmq/pause.ts
  • src/lib/server/workflow/state-machine.ts
📚 Learning: 2025-09-12T14:02:04.558Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/lib/server/workflow/index.ts:52-74
Timestamp: 2025-09-12T14:02:04.558Z
Learning: In the appbuilder-portal codebase, when a Product exists, it always has an associated Project relationship. The Project._count fields (Authors, Reviewers) are safe to access directly when the Product query returns a result.

Applied to files:

  • src/lib/server/workflow/index.ts
📚 Learning: 2025-05-12T13:19:42.118Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1115
File: portal/common/bullmq/types.ts:205-220
Timestamp: 2025-05-12T13:19:42.118Z
Learning: In the AppBuilder portal, `System_CheckEngineStatuses` and `System_RefreshLangTags` job types are designed to run both periodically (as recurring jobs) and immediately upon startup, which is why they're included in both the `RecurringJob` and `StartupJob` type definitions.

Applied to files:

  • src/lib/server/bullmq/types.ts
📚 Learning: 2025-09-04T16:23:55.891Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1227
File: src/lib/server/job-executors/product.ts:247-250
Timestamp: 2025-09-04T16:23:55.891Z
Learning: In src/lib/server/job-executors/product.ts, createLocal’s catch should log the error via job.log with relevant context (projectId/productDefinitionId/storeId) and still return false to preserve the intentional “no-retry” behavior.

Applied to files:

  • src/lib/server/job-executors/product.ts
  • src/lib/server/workflow/state-machine.ts
🧬 Code graph analysis (3)
src/lib/products/server.ts (1)
src/lib/server/workflow/index.ts (1)
  • Workflow (36-592)
src/lib/server/workflow/index.ts (1)
src/lib/workflowTypes.ts (2)
  • WorkflowConfig (183-188)
  • WorkflowInstanceContext (105-138)
src/lib/server/workflow/state-machine.ts (4)
src/lib/server/bullmq/types.ts (3)
  • Build (101-108)
  • Product (77-82)
  • Product (165-171)
src/lib/server/bullmq/queues.ts (1)
  • getQueues (150-155)
src/lib/workflowTypes.ts (1)
  • autoPublishOnRebuild (268-274)
src/lib/server/workflow/dbProcedures.ts (1)
  • notifyAutoPublishOwner (55-82)
🪛 Biome (2.1.2)
src/lib/products/server.ts

[error] 11-11: expected , but instead found parentJobId

Remove parentJobId

(parse)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks / build-and-test
🔇 Additional comments (22)
src/lib/server/email-service/locales/fr-FR.json (1)

119-120: Localization additions look good.

The French translations are grammatically sound and properly structured. The new autoPublishOnRebuildCompleted entries in both the subject and notification sections are consistent with the template variables and formatting used elsewhere in the file.

src/lib/server/email-service/locales/es-419.json (2)

77-77: Missing localization entry in body section.

Per the AI summary, autoPublishOnRebuildCompleted should be added to three sections (body, subject, and notification), but this code shows only subject (line 77) and notification (line 120) additions. The body section (lines 3–46) appears to lack the corresponding entry.

Verify that the body section includes "autoPublishOnRebuildCompleted" with appropriate Spanish text describing the automatic publication completion.

Also applies to: 120-120


77-77: Spanish translations are grammatically sound and follow localization patterns.

Both translations correctly use template variables and follow the established naming and text conventions:

  • Line 77 (subject): Includes "Scriptoria:" prefix, consistent with other subject entries
  • Line 120 (notification): No prefix, consistent with other notification entries
  • Template variables {{productName}} and {{projectName}} are correctly positioned

Also applies to: 120-120

src/lib/server/job-executors/product.ts (1)

247-249: LGTM!

The addition of isAutomatic: false correctly satisfies the updated WorkflowConfig type requirement. This is appropriate for locally-created products which are not part of automated rebuild flows.

src/lib/products/server.ts (2)

55-66: LGTM!

The Workflow.create call correctly passes the new parentJobId and isAutomatic fields in the config object, and the comment as a separate argument. This aligns with the updated signature in src/lib/server/workflow/index.ts.


92-92: Good addition for audit trail.

Adding the Comment field to the Cancel transition record improves traceability by capturing the reason for cancellation.

src/lib/server/workflow/dbProcedures.ts (1)

55-82: LGTM!

The notifyAutoPublishOwner function is well-implemented with proper null guards and follows existing notification patterns in the codebase. The early return on missing OwnerId prevents unnecessary queue operations.

src/lib/server/job-executors/system.ts (1)

954-962: LGTM!

Adding isAutomatic: false to the migrated workflow context is correct. Legacy migrated instances should default to non-automatic since they were not created through the new automated rebuild flow.

src/lib/server/workflow/index.ts (4)

51-55: LGTM!

The updated create method signature properly accepts parentJobId via the config extension and comment as a separate parameter. This design cleanly separates workflow configuration from transition metadata.


74-81: LGTM!

The workflow input correctly propagates parentJobId and autoPublishOnRebuild from the project settings. The double-negation !!check?.Project.AutoPublishOnRebuild safely converts to boolean.


193-194: Good backward compatibility handling.

The nullish coalescing assignment context.isAutomatic ??= false ensures that workflows restored from snapshots created before this change will have a valid boolean value, preventing runtime errors in downstream logic.


373-382: LGTM!

Correctly excludes runtime-derived fields (autoPublishOnRebuild, productType, options, etc.) from the persisted context. These fields are re-computed on restore from the database relationships.

src/lib/server/bullmq/types.ts (4)

37-39: LGTM!

The Rebuild_Parent job type is appropriately placed with the build jobs and clearly documented as a parent job for admin-initiated rebuilds.


90-97: LGTM!

The Build.Parent interface is well-designed with a required projectId for identifying the rebuild scope and an optional initiatedBy for audit tracking. The comment clarifies this is a grouping type not processed by build workers.


342-342: LGTM!

The JobTypeMap correctly maps Rebuild_Parent to Build.Parent, ensuring type safety throughout the codebase.


305-308: Verify the parent job processing is handled appropriately.

The Rebuild_Parent job is included in BuildJob union. Ensure the build worker has logic to handle or skip this job type, since per the comment it's "not processed by build workers" but is a grouping parent job.

src/lib/workflowTypes.ts (1)

137-137: LGTM! Well-structured type extensions and guard implementation.

The new fields (isAutomatic, parentJobId, autoPublishOnRebuild) integrate cleanly into the existing type hierarchy, and the autoPublishOnRebuild guard function correctly validates all three required conditions (autoPublishOnRebuild flag, isAutomatic flag, and Rebuild workflow type).

Also applies to: 187-187, 194-196, 268-275

src/lib/server/email-service/locales/en-us.json (1)

77-77: LGTM! Localization keys properly structured.

The new auto-publish notification keys follow the existing naming conventions and include appropriate placeholders for project and product names.

Also applies to: 119-120

src/lib/server/workflow/state-machine.ts (4)

61-61: LGTM! Context properly initialized with new fields.

The context initialization correctly propagates isAutomatic and autoPublishOnRebuild from the input, enabling the workflow to support automatic rebuild and auto-publish functionality.

Also applies to: 68-68


504-531: LGTM! Clean refactor with proper parent job linkage.

The refactored payload construction improves clarity by separating data and options objects. The conditional parent linkage (lines 527-529) correctly associates child build jobs with the parent rebuild job when parentJobId is provided.


556-565: LGTM! Auto-transition correctly gated.

The new transition to Product_Publish is properly guarded using the autoPublishOnRebuild function from workflowTypes, ensuring automatic publishing only occurs when all conditions are met (autoPublishOnRebuild flag, isAutomatic flag, and Rebuild workflow type).


840-857: LGTM! Notifications properly gated.

The notifyAutoPublishOwner calls are correctly conditional on both autoPublishOnRebuild and isAutomatic flags, ensuring product owners are notified only when automatic publishing completes on rebuild workflows.

Comment on lines 28 to 51
// --- 1. Read children BEFORE removing them ---
const children = await parentJob.getChildrenValues<{
name: string;
data: BuildJob;
opts?: JobsOptions;
}>();

// children is an object keyed by childJobId → { name, data, opts }
const removedChildren = Object.values(children);

// --- 2. Remove all unprocessed child jobs ---
const allJobs = await Builds.getJobs(['waiting', 'delayed']);
const childJobs = allJobs.filter((job) => job.opts?.parent?.id === parentJobId);

for (const job of childJobs) {
await job.remove();
}

// --- 3. Move parent to delayed ---
await parentJob.moveToDelayed(Date.now() + 24 * 3600 * 1000);

OTEL.instance.logger.info(`Paused rebuild: removed ${removedChildren.length} children`);

return removedChildren; // return so unpauseRebuild can accept it
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Mismatch between removed children and returned metadata.

The function retrieves metadata for all children (line 29-36) but only removes children in waiting or delayed states (lines 39-44). This creates a mismatch: removedChildren includes metadata for jobs that weren't actually removed (e.g., active or completed jobs). When unpauseRebuild later recreates all jobs from this metadata, it will attempt to recreate jobs that may still exist in other states, leading to duplicates or inconsistent parent-child relationships.

Apply this diff to only return metadata for jobs that were actually removed:

-  // children is an object keyed by childJobId → { name, data, opts }
-  const removedChildren = Object.values(children);
-
   // --- 2. Remove all unprocessed child jobs ---
   const allJobs = await Builds.getJobs(['waiting', 'delayed']);
   const childJobs = allJobs.filter((job) => job.opts?.parent?.id === parentJobId);
 
+  // Collect metadata only for jobs we're actually removing
+  const removedChildren = [];
   for (const job of childJobs) {
+    const child = children[job.id];
+    if (child) {
+      removedChildren.push(child);
+    }
     await job.remove();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// --- 1. Read children BEFORE removing them ---
const children = await parentJob.getChildrenValues<{
name: string;
data: BuildJob;
opts?: JobsOptions;
}>();
// children is an object keyed by childJobId → { name, data, opts }
const removedChildren = Object.values(children);
// --- 2. Remove all unprocessed child jobs ---
const allJobs = await Builds.getJobs(['waiting', 'delayed']);
const childJobs = allJobs.filter((job) => job.opts?.parent?.id === parentJobId);
for (const job of childJobs) {
await job.remove();
}
// --- 3. Move parent to delayed ---
await parentJob.moveToDelayed(Date.now() + 24 * 3600 * 1000);
OTEL.instance.logger.info(`Paused rebuild: removed ${removedChildren.length} children`);
return removedChildren; // return so unpauseRebuild can accept it
// --- 1. Read children BEFORE removing them ---
const children = await parentJob.getChildrenValues<{
name: string;
data: BuildJob;
opts?: JobsOptions;
}>();
// --- 2. Remove all unprocessed child jobs ---
const allJobs = await Builds.getJobs(['waiting', 'delayed']);
const childJobs = allJobs.filter((job) => job.opts?.parent?.id === parentJobId);
// Collect metadata only for jobs we're actually removing
const removedChildren = [];
for (const job of childJobs) {
const child = children[job.id];
if (child) {
removedChildren.push(child);
}
await job.remove();
}
// --- 3. Move parent to delayed ---
await parentJob.moveToDelayed(Date.now() + 24 * 3600 * 1000);
OTEL.instance.logger.info(`Paused rebuild: removed ${removedChildren.length} children`);
return removedChildren; // return so unpauseRebuild can accept it
🤖 Prompt for AI Agents
In src/lib/server/bullmq/pause.ts around lines 28 to 51, the code returns
metadata for all children but only removes jobs in 'waiting' or 'delayed',
causing a mismatch; fix by collecting the actual IDs of the jobs you remove
(after filtering allJobs by parent and iterating removals), then filter the
previously-read children metadata to only include entries whose job IDs are in
that removed-ID set before logging and returning — ensure the return value only
contains metadata for jobs you actually removed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/routes/(authenticated)/products/[id]/files/+page.server.ts (1)

93-106: Calling requireProjectReadAccess without project will always 404

Here project is correctly fetched and validated:

const project = (await DatabaseReads.products.findUnique(...))?.Project;
if (!project) error(404);

But the subsequent call omits it:

locals.security.requireProjectReadAccess(
  await DatabaseReads.groupMemberships.findMany({ ... })
  // project not passed here
);

Given the current Security.requireProjectReadAccess implementation, a missing project argument is treated as falsy and results in error(404, 'Project not found'), so this action will always fail with 404.

You likely meant to keep passing project here. Suggested fix:

-    locals.security.requireProjectReadAccess(
-      await DatabaseReads.groupMemberships.findMany({
-        where: { UserId: locals.security.userId, GroupId: project.GroupId }
-      })
-    );
+    locals.security.requireProjectReadAccess(
+      await DatabaseReads.groupMemberships.findMany({
+        where: { UserId: locals.security.userId, GroupId: project.GroupId }
+      }),
+      project
+    );
🧹 Nitpick comments (6)
src/lib/server/database/Projects.ts (1)

50-58: Reassign userMapping change looks good; consider guarding against null existing

  • The updated userMapping: [{ from: existing!.OwnerId, to: ownerId }] correctly matches the narrowed { from, to }[] type for the Reassign operation and preserves existing behavior (single mapping from previous owner to new owner). No issues with the change itself.

  • Independently of this PR, existing is nullable (findUnique can return null), but it’s used with a non‑null assertion both when computing orgId/groupId/ownerId and in userMapping. If id were ever invalid or the project deleted concurrently, this would throw before the try/catch and surface as an unhandled error.

    You could defensively guard once after the lookup and then drop the !:

    const existing = await prisma.projects.findUnique({
      where: {
        Id: id
      }
    });
  • if (!existing) {
  • return false;
  • }
  • const orgId = projectData.OrganizationId ?? existing!.OrganizationId;
  • const groupId = projectData.GroupId ?? existing!.GroupId;
  • const ownerId = projectData.OwnerId ?? existing!.OwnerId;
  • const orgId = projectData.OrganizationId ?? existing.OrganizationId;
  • const groupId = projectData.GroupId ?? existing.GroupId;
  • const ownerId = projectData.OwnerId ?? existing.OwnerId;
  •      userMapping: [{ from: existing!.OwnerId, to: ownerId }]
    
  •      userMapping: [{ from: existing.OwnerId, to: ownerId }]
    
    
    That keeps the new job payload shape while making the function safer under unexpected IDs.
    
    
    
    
    
    

Also applies to: 70-78

src/lib/server/job-executors/userTasks.ts (1)

62-87: Reassign operation lacks validation; confirm this design is intentional.

The reassignment logic (lines 62-87) bypasses the role validation that exists in the Create/Update/Delete path (line 173: .filter((t) => allUsers[t.UserId].has(t.Role))).

However, this appears intentional rather than accidental. Reassign is semantically different: it transfers existing tasks from one specific user to another via direct user mapping, whereas Create/Update/Delete regenerate tasks based on role eligibility. The current sole caller (Projects.ts) reassigns tasks during owner changes, where the new owner implicitly has the required role.

The concern is whether this design will remain valid if Reassign is used elsewhere. Consider adding:

  1. Documentation clarifying why Reassign has no role check
  2. Validation at the call site to ensure reassignments are to users with compatible roles (if needed for future use cases)
src/lib/server/job-executors/email.ts (1)

97-126: Reviewer and owner email construction is consistent; minor duplication opportunity only

Using product.Project.Reviewers and product.Project.Owner directly for recipient lists, locale selection, and owner-specific reviewerNames is consistent with the rest of this module and should behave as expected, including the case with zero reviewers (owner email still sent).

If you touch this again, consider extracting a tiny helper to build the shared properties / reviewerNames so the reviewer and owner branches don’t need to recompute similar data, but this is purely a readability win and not required for this PR.

src/lib/server/build-engine-api/types.ts (1)

67-72: Consider adding runtime validation or explicit property mapping.

The toStatusResponse function uses a type assertion with the spread operator, which bypasses TypeScript's type checking. This allows any properties from body to leak through without validation, potentially violating the StatusResponse contract (e.g., wrong types for versions, created, or updated).

Consider either:

  • Explicitly mapping known properties: { responseType: 'status', status, versions: body.versions as StatusResponse['versions'], ... }
  • Adding runtime validation to ensure body matches the expected shape

Example with explicit mapping:

 export function toStatusResponse(
   status: number,
   body: Record<string, unknown> = {}
 ): StatusResponse {
-  return { responseType: 'status', status, ...body } as StatusResponse;
+  return {
+    responseType: 'status',
+    status,
+    ...(body.versions && { versions: body.versions as StatusResponse['versions'] }),
+    ...(body.created && { created: body.created as string }),
+    ...(body.updated && { updated: body.updated as string })
+  };
 }
src/auth.ts (1)

255-267: Optional project parameter does not match runtime behavior

requireProjectReadAccess now types project as optional, but the implementation still hard-fails with a 404 whenever project is falsy. That means any caller that relies on the new optional signature and omits project will always get a 404, even when a valid project was already fetched separately.

Either keep the parameter required in the type, or update the implementation to have a well-defined behavior when project is omitted (e.g., treat “no project argument” differently from “project not found”). As it stands, the optional typing is misleading and has already enabled at least one broken call site.

src/app.d.ts (1)

28-35: Security type now allows an unsafe call pattern

The global Security.requireProjectReadAccess signature now makes project optional, even though the concrete implementation still errors when project is falsy. This loosened type is what allowed a call without project in products/[id]/files/+page.server.ts to compile but fail at runtime.

To keep TS honest, consider either:

  • Making project required again in this declaration, or
  • Explicitly documenting and implementing a distinct behavior for project === undefined vs. project === null (e.g., overloads), if you truly need a “no-project argument” variant.

Right now the type surface doesn’t reflect the actual behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e9e57eb and 2cb79b0.

📒 Files selected for processing (34)
  • .github/workflows/setup.yml (2 hunks)
  • Dockerfile (0 hunks)
  • README.md (0 hunks)
  • run (0 hunks)
  • src/app.d.ts (1 hunks)
  • src/auth.ts (1 hunks)
  • src/lib/components/settings/Toggle.svelte (1 hunks)
  • src/lib/prisma/migrations/18_system_version_hash/migration.sql (0 hunks)
  • src/lib/prisma/migrations/19_track_updates/migration.sql (0 hunks)
  • src/lib/prisma/schema.prisma (0 hunks)
  • src/lib/products/index.ts (0 hunks)
  • src/lib/products/server.ts (3 hunks)
  • src/lib/projects/sse.ts (1 hunks)
  • src/lib/server/build-engine-api/types.ts (2 hunks)
  • src/lib/server/bullmq/types.ts (1 hunks)
  • src/lib/server/database/Projects.ts (1 hunks)
  • src/lib/server/email-service/locales/en-us.json (2 hunks)
  • src/lib/server/email-service/locales/es-419.json (2 hunks)
  • src/lib/server/email-service/locales/fr-FR.json (2 hunks)
  • src/lib/server/job-executors/build.ts (3 hunks)
  • src/lib/server/job-executors/email.ts (4 hunks)
  • src/lib/server/job-executors/system.ts (1 hunks)
  • src/lib/server/job-executors/userTasks.ts (3 hunks)
  • src/lib/server/workflow/index.ts (4 hunks)
  • src/lib/server/workflow/state-machine.ts (1 hunks)
  • src/lib/workflowTypes.ts (0 hunks)
  • src/routes/(authenticated)/+layout.svelte (2 hunks)
  • src/routes/(authenticated)/products/[id]/files/+page.server.ts (1 hunks)
  • src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts (1 hunks)
  • src/routes/(authenticated)/projects/[id=idNumber]/modals/Properties.svelte (3 hunks)
  • src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Configuration.svelte (3 hunks)
  • src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Verify_And_Publish.svelte (1 hunks)
  • src/routes/(authenticated)/tasks/[product_id]/instructions/Verify_And_Publish.svelte (1 hunks)
  • src/routes/(unauthenticated)/docs/[filename]/+server.ts (0 hunks)
💤 Files with no reviewable changes (9)
  • README.md
  • src/routes/(unauthenticated)/docs/[filename]/+server.ts
  • Dockerfile
  • run
  • src/lib/workflowTypes.ts
  • src/lib/products/index.ts
  • src/lib/prisma/migrations/18_system_version_hash/migration.sql
  • src/lib/prisma/schema.prisma
  • src/lib/prisma/migrations/19_track_updates/migration.sql
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/server/bullmq/types.ts
  • src/lib/server/email-service/locales/fr-FR.json
  • src/lib/server/email-service/locales/es-419.json
🧰 Additional context used
🧠 Learnings (11)
📚 Learning: 2025-09-04T14:26:59.326Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1227
File: src/lib/server/job-executors/product.ts:247-250
Timestamp: 2025-09-04T14:26:59.326Z
Learning: In src/lib/server/job-executors/product.ts, the createLocal function's catch block returns false instead of rethrowing errors. This was implemented intentionally to fix another issue, so any changes to this error handling should be carefully evaluated for downstream impacts.

Applied to files:

  • src/lib/server/job-executors/build.ts
  • src/lib/server/job-executors/email.ts
  • src/lib/server/workflow/state-machine.ts
  • src/lib/server/job-executors/system.ts
📚 Learning: 2025-09-04T16:23:55.891Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1227
File: src/lib/server/job-executors/product.ts:247-250
Timestamp: 2025-09-04T16:23:55.891Z
Learning: In src/lib/server/job-executors/product.ts, createLocal’s catch should log the error via job.log with relevant context (projectId/productDefinitionId/storeId) and still return false to preserve the intentional “no-retry” behavior.

Applied to files:

  • src/lib/server/job-executors/build.ts
📚 Learning: 2025-09-10T13:51:14.948Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/routes/(authenticated)/projects/[id=idNumber]/+page.svelte:191-193
Timestamp: 2025-09-10T13:51:14.948Z
Learning: In the appbuilder-portal codebase, the `productsToAdd` property in project data is guaranteed to always be an array (possibly empty), never undefined.

Applied to files:

  • src/lib/server/job-executors/build.ts
📚 Learning: 2025-12-04T23:01:23.843Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1384
File: src/lib/server/workflow/index.ts:92-103
Timestamp: 2025-12-04T23:01:23.843Z
Learning: In src/lib/server/workflow/index.ts, the Workflow.create method intentionally creates a snapshot before creating product transitions (around line 83) and another after starting the flow (around line 103). The first snapshot is required to prevent errors when the initial state is Product Build (as in Rebuild workflows). The second snapshot ensures the user tasks job has up-to-date information after transitions are created and the flow is started.

Applied to files:

  • src/lib/server/job-executors/build.ts
  • src/lib/server/workflow/index.ts
  • src/lib/products/server.ts
  • src/lib/server/workflow/state-machine.ts
📚 Learning: 2025-09-25T19:59:36.396Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts:22-29
Timestamp: 2025-09-25T19:59:36.396Z
Learning: In the AppBuilder portal project, the Security type is declared globally in src/app.d.ts and does not need to be explicitly imported when used in TypeScript files throughout the application.

Applied to files:

  • src/app.d.ts
📚 Learning: 2025-09-12T14:07:02.200Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:285-303
Timestamp: 2025-09-12T14:07:02.200Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to optimize filterAvailableActions by creating Sets at the caller level rather than inside the function, so the function would take Set<number> arguments instead of arrays for better performance and separation of concerns.

Applied to files:

  • src/routes/(authenticated)/products/[id]/files/+page.server.ts
  • src/lib/products/server.ts
  • src/lib/server/job-executors/userTasks.ts
  • src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts
📚 Learning: 2025-09-12T14:57:08.647Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:252-264
Timestamp: 2025-09-12T14:57:08.647Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to use transition.target[0] prediction mechanism for determining post-send state rather than calling flow.state() after flow.send() due to race condition concerns. The prediction approach is sufficient for redirect decisions between staying on task page vs going to projects page.

Applied to files:

  • src/routes/(authenticated)/products/[id]/files/+page.server.ts
  • src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts
  • src/lib/server/workflow/state-machine.ts
📚 Learning: 2025-09-10T13:46:43.497Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/auth.ts:240-245
Timestamp: 2025-09-10T13:46:43.497Z
Learning: In src/auth.ts, the products branch intentionally uses params.id as a string for database lookups, not as a number. This is the correct behavior and should not be changed to numeric parsing.

Applied to files:

  • src/routes/(authenticated)/products/[id]/files/+page.server.ts
📚 Learning: 2025-10-13T17:26:55.905Z
Learnt from: chrisvire
Repo: sillsdev/appbuilder-portal PR: 1343
File: src/routes/(unauthenticated)/api/auth/token/+server.ts:15-19
Timestamp: 2025-10-13T17:26:55.905Z
Learning: In src/routes/(unauthenticated)/api/auth/token/+server.ts, the GET handler correctly uses requireNothing() at the beginning because this endpoint initiates the authentication flow for AppBuilders. Auth establishment endpoints should allow unauthenticated access initially, as users may need to be redirected to login.

Applied to files:

  • src/routes/(authenticated)/products/[id]/files/+page.server.ts
📚 Learning: 2025-09-22T14:59:53.366Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts:214-221
Timestamp: 2025-09-22T14:59:53.366Z
Learning: In the AppBuilder portal, bulk project actions in the route `/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts` are organization-scoped by design. The route parameter `[orgId=idNumber]` ensures all operations happen within a single organization context, so using the route-level orgId for all projects in bulk actions like `doProjectAction` is correct and safe.

Applied to files:

  • src/routes/(authenticated)/products/[id]/files/+page.server.ts
  • src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts
📚 Learning: 2025-09-12T14:02:04.558Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/lib/server/workflow/index.ts:52-74
Timestamp: 2025-09-12T14:02:04.558Z
Learning: In the appbuilder-portal codebase, when a Product exists, it always has an associated Project relationship. The Project._count fields (Authors, Reviewers) are safe to access directly when the Product query returns a result.

Applied to files:

  • src/lib/server/job-executors/userTasks.ts
  • src/lib/projects/sse.ts
🧬 Code graph analysis (5)
src/lib/products/server.ts (1)
src/lib/server/workflow/index.ts (1)
  • Workflow (36-582)
src/lib/server/job-executors/userTasks.ts (2)
src/lib/server/database/prisma.ts (1)
  • DatabaseReads (22-22)
src/lib/server/database/index.ts (1)
  • DatabaseWrites (68-71)
src/lib/server/job-executors/email.ts (4)
src/lib/server/job-executors/build.ts (1)
  • product (11-131)
src/lib/server/job-executors/publish.ts (1)
  • product (10-158)
src/lib/server/email-service/locales/locale.ts (1)
  • translate (35-54)
src/lib/server/email-service/EmailTemplates.ts (3)
  • addProperties (23-36)
  • ReviewProductTemplate (19-19)
  • NotificationTemplate (14-14)
src/lib/server/workflow/state-machine.ts (1)
src/lib/server/bullmq/queues.ts (1)
  • getQueues (150-155)
src/lib/server/job-executors/system.ts (2)
src/lib/server/database/prisma.ts (1)
  • DatabaseReads (22-22)
src/lib/server/database/index.ts (1)
  • DatabaseWrites (68-71)
🔇 Additional comments (22)
src/lib/server/email-service/locales/en-us.json (1)

44-44: Clean removal of retryBuild infrastructure from locale strings.

The removal of retryBuild notification entries from both notifications.body and notifications.subject is correctly implemented, with proper JSON formatting (no trailing commas on the final entries in each section). The recordNotFound entries serve as appropriate fallback messages for missing record scenarios and align well with the broader PR objective to simplify retry-based workflows.

Also applies to: 85-85

src/lib/components/settings/Toggle.svelte (1)

47-48: Icon order & active-state styling may invert visual on/off semantics

Swapping the order and color="white" between onIcon and offIcon will change which icon is emphasized and potentially which one appears in the “on” vs “off” state, depending on how .toggle is styled (e.g., sibling selectors or first/last-child rules). Please double-check the rendered toggle in both states to ensure this matches the intended design for all usages of Toggle.

src/lib/server/job-executors/system.ts (1)

157-196: The current design handles versioning correctly; race conditions are unlikely and error handling is atomic.

The pre-fetch at line 157 combined with subsequent updates is safe given BullMQ's default concurrency of 1 for this worker, so concurrent execution is not a concern. The lack of try-catch is intentional—if any database operation fails, the job fails atomically rather than leaving partial updates. If partial-failure tolerance is desired (updating some versions while skipping failed ones), consider using Promise.allSettled() instead of Promise.all() and filtering for errors.

src/lib/server/job-executors/userTasks.ts (2)

6-6: LGTM: Type-only import is appropriate.

The change to a type-only import for RoleId is correct since it's only used for type assertions (line 151) and not as a runtime value. This improves tree-shaking and makes the type-level usage explicit.


28-28: LGTM: Removal of unused field improves query efficiency.

The OrganizationId field is not referenced anywhere in the function body, so removing it from the select clause is a good optimization.

src/lib/projects/sse.ts (1)

36-40: Select ApplicationTypeId instead of Description for the filter

The concern about using Description as a filter is valid—descriptions are typically non-unique and fragile for filtering. However, the current code is actually using valid Prisma syntax for a to-one relation filter (not a list relation), so the suggestion to use some: { ... } is incorrect and would cause a type error.

Instead, update the select to include the Id:

          ApplicationType: {
            select: {
+             Id: true,
              Description: true
            }
          },

Then filter by the relation's Id directly (without some, which applies only to list relations):

      const productDefinitions = (
        await DatabaseReads.organizationProductDefinitions.findMany({
          where: {
            OrganizationId: project.OrganizationId,
            ProductDefinition: {
-             ApplicationTypes: project.ApplicationType
+             ApplicationTypes: {
+               Id: project.ApplicationType.Id
+             }
            }
          },

Likely an incorrect or invalid review comment.

src/lib/server/job-executors/email.ts (2)

56-95: Product fetch, artifact URL extraction, and comment-aware messageId look solid; verify i18n coverage

The switch to including Project (with Organization, Reviewers, Owner), ProductArtifacts, and ProductDefinition plus deriving URLs via ProductArtifacts.find(...) is correct and clearer, and using product.WorkflowComment to append WithComment to messageId and populate comment in properties keeps email content aligned with stored workflow state.

Please double‑check that all notifications.subject.* / notifications.body.* keys exist for the new *WithComment variants (e.g., reviewProductWithComment, reviewPwaProductWithComment, etc.) so we don’t hit missing-translation or template-property errors at runtime.


239-248: Low‑priority super‑admin emails now ignore link and always use NotificationTemplate; ensure callers embed links in the body

Always using NotificationTemplate and dropping LinkUrl / UrlText parameters simplifies this path and avoids dependency on the link-specific template, assuming any URLs are now baked into the localized notifications.body.<messageKey> strings via messageProperties.

Please confirm that no existing producers of NotifySuperAdminsLowPriority still expect a clickable link supplied via a separate link field; if they do, their templates or message properties will need updating to keep the link visible.

src/routes/(authenticated)/projects/[id=idNumber]/modals/Properties.svelte (1)

80-83: Event wiring for compute type selection/default looks good

The onchange handler calling updateComputeType(e.currentTarget.value as ComputeType) together with bind:value={computeType}, and the "Default" button calling updateComputeType() are consistent and keep the select and JSON in sync once the parsing issue above is addressed. No changes needed here from a logic standpoint.

Also applies to: 113-114

src/routes/(authenticated)/tasks/[product_id]/instructions/Verify_And_Publish.svelte (1)

10-16: LGTM!

The documentation URL update to scriptoria.io/docs is consistent with the broader domain consolidation across the PR.

.github/workflows/setup.yml (2)

154-154: LGTM!

The double-quote formatting for environment variable values is a minor style consistency improvement.


182-184: LGTM!

Consistent quoting style for test environment variables.

src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Verify_And_Publish.svelte (1)

14-18: LGTM!

The documentation URL update to scriptoria.io/docs is consistent with the broader domain consolidation, and the single-line anchor formatting improves readability.

src/lib/server/job-executors/build.ts (3)

8-8: LGTM!

Import simplified to only include fetchPackageName, consistent with the removal of compute-type management functionality.


151-153: LGTM!

The Properties field was removed from the query as it's no longer needed after removing the compute-type retry logic.


279-285: The structured comment format is handled correctly downstream.

The TaskComment component (src/lib/products/components/TaskComment.svelte) explicitly parses the system.build-failed,{consoleText} format, extracting the URL and presenting it as a clickable link to the console text. The change is compatible with existing downstream processing.

src/lib/server/workflow/state-machine.ts (1)

767-772: The code change is correct. The type definition for Email.SendNotificationToReviewers at src/lib/server/bullmq/types.ts:234-236 only requires type and productId fields—no comment field exists in the interface. The payload being sent matches the type definition exactly.

src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Configuration.svelte (1)

4-35: Content and link updates look consistent

The revised navigation text, Play listing wording, keystore guidance, and updated documentation URL all look coherent and consistent with the rest of the instructions. No code-level issues here.

src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts (1)

56-57: Rebuild toggles are now dev-only

showRebuildToggles is restricted to env.NODE_ENV === 'development', which will hide these controls in staging/preview/prod regardless of any APP_ENV-style flags you may have. If you still need to exercise rebuild toggles outside local dev, consider widening this condition (e.g., to include specific non-prod environments).

src/lib/products/server.ts (1)

7-11: Comment propagation through product actions looks correct

The extended doProductAction signature with comment?: string cleanly threads the comment into Workflow.create for Rebuild/Republish and into the CancelWorkflow ProductTransitions record. Existing call sites that don’t care about comments remain compatible since the parameter is optional.

Also applies to: 53-61, 81-88

src/routes/(authenticated)/+layout.svelte (1)

220-229: Admin software-update nav and help URL change are consistent

The new /software-update nav item is correctly gated by the existing admin check, uses isUrlActive/activeOrgUrl, and follows the same drawer-close pattern as other admin links. The updated Help link URL is a straightforward target change with no code-side impact.

Also applies to: 341-344

src/lib/server/workflow/index.ts (1)

51-56: Workflow comment plumbing and state-change simplification look sound

The optional comment parameter on Workflow.create is correctly persisted on the initial StartWorkflow transition and then reused via inspect/updateProductTransitions and the UserTasks update job. The stateChange simplification to a straight state-name comparison, along with the adjusted visualization logic for transitions without explicit targets, is consistent with the removal of retry-specific behavior and shouldn’t affect normal flows.

Also applies to: 90-96, 237-261, 287-287, 355-359

Comment on lines 53 to 65
export type StatusResponse = {
responseType: 'status';
status: number;
// these should be left optional until BuildEngine supports version info
versions?: {
scriptureappbuilder?: string;
readingappbuilder?: string;
dictionaryappbuilder?: string;
keyboardappbuilder?: string;
};
created?: string;
updated?: string;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the types.ts file to see the actual definitions
cat -n src/lib/server/build-engine-api/types.ts

Repository: sillsdev/appbuilder-portal

Length of output: 6175


🏁 Script executed:

# Search for StatusResponse and toStatusResponse usage
rg -n 'StatusResponse|toStatusResponse' --type=ts

# Also check for any timestamp parsing or conversion
rg -n -C2 'created.*Date|updated.*Date' --type=ts | head -50

Repository: sillsdev/appbuilder-portal

Length of output: 975


🏁 Script executed:

# Check the context around toStatusResponse call in requests.ts
sed -n '140,155p' src/lib/server/build-engine-api/requests.ts

Repository: sillsdev/appbuilder-portal

Length of output: 608


🏁 Script executed:

# Check if there's any timestamp parsing or conversion happening
rg -n 'new Date|Date\.parse|parseDate' --type=ts

# Also check the converter functions to see if any do special handling
rg -n -A5 'function to.*Response' --type=ts | grep -A5 'Date\|created\|updated'

Repository: sillsdev/appbuilder-portal

Length of output: 2966


🏁 Script executed:

# Check how the timestamps from API responses are used
# Look for code that accesses .created or .updated from response objects
rg -n -B2 -A2 '\.created|\.updated' src/lib/server/build-engine-api/ --type=ts

# Also check how ProjectResponse, BuildResponse etc are used
rg -n 'toProjectResponse|toJobResponse|toBuildResponse' --type=ts -A3 | head -40

Repository: sillsdev/appbuilder-portal

Length of output: 2789


🏁 Script executed:

# Check if created/updated from SuccessResponse types are actually used
# and if they're treated as Date objects anywhere
rg -n 'project\.created|project\.updated|build\.created|build\.updated|job\.created|job\.updated' --type=ts -B1 -A1

# Also check database/ORM models to see what types they expect
fd -e ts -e js 'database\|models\|schema' | head -20

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Find database-related files
find . -type f -name "*.ts" -path "*/database/*" | head -20

# Also check for any ORM/database schema definitions
rg -n 'created|updated' --type=ts -l src/lib/server/database/ | head -5

Repository: sillsdev/appbuilder-portal

Length of output: 946


🏁 Script executed:

# Check the Prisma schema to understand database timestamp expectations
find . -type f -name "schema.prisma" | head -5

# Also check Projects.ts to see how API responses are stored
sed -n '1,50p' src/lib/server/database/Projects.ts

Repository: sillsdev/appbuilder-portal

Length of output: 1673


🏁 Script executed:

# Check the Prisma schema to see timestamp field types
cat src/lib/prisma/schema.prisma | grep -A 50 "model Project"

Repository: sillsdev/appbuilder-portal

Length of output: 3935


🏁 Script executed:

# Check how the timestamp data from API responses flows to the database
# Look for where toProjectResponse results are used
rg -n -B5 -A5 'toProjectResponse\|toJobResponse\|toBuildResponse' src/lib/server/build-engine-api/requests.ts | head -60

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Search for where the API responses are consumed
rg -n 'systemCheck\|getProject\|getJob\|getBuild' src/ --type=ts | head -20

# Also check if timestamps from API are ever directly used
rg -n 'response\.created\|response\.updated' src/ --type=ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Find where systemCheck is called
rg -n 'systemCheck' src/ --type=ts -B2 -A2

# Also check the actual API response structure by looking at BuildEngine documentation or examples
rg -n 'versionInfo\|version' src/lib/server/ --type=ts | head -20

Repository: sillsdev/appbuilder-portal

Length of output: 910


🏁 Script executed:

# Check the system.ts file where systemCheck is called and how the result is used
sed -n '95,160p' src/lib/server/job-executors/system.ts

Repository: sillsdev/appbuilder-portal

Length of output: 2126


🏁 Script executed:

# Check how versionInfo.updated is used and confirm it's a string in StatusResponse
rg -n 'versionInfo.*updated\|\.updated' src/lib/server/job-executors/system.ts -B2 -A2

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Let me check if there's actual usage of the created/updated fields from SuccessResponse types
rg -n 'created|updated' src/lib/server/build-engine-api/requests.ts -B2 -A2

# Also verify what the actual API response looks like by checking test data or mock responses
rg -n 'mock\|fixture\|test' src/lib/server/build-engine-api/ --type=ts | head -10

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check all the places where StatusResponse or SuccessResponse timestamps might be used
rg -n 'response.*created\|response.*updated' src/lib/server/ --type=ts -B1 -A1

# Also check if there are tests that show what the API actually returns
find . -type f -name "*.test.ts" -o -name "*.spec.ts" | head -10

Repository: sillsdev/appbuilder-portal

Length of output: 89


SuccessResponse timestamp types are incorrect; they should be strings, not Dates.

StatusResponse correctly defines created and updated as optional strings (lines 63-64), matching the JSON response format from BuildEngine. However, SuccessResponse incorrectly declares these as required Date objects (lines 86-87), even though the API returns strings.

The converters toProjectResponse, toJobResponse, and toBuildResponse use type assertions (as ProjectResponse, etc.) without actually parsing the timestamp strings to Date objects. This creates a type safety gap: the types claim Date objects, but the actual data contains strings.

Either:

  1. Change SuccessResponse to use optional strings and parse timestamps in the converters (like line 152 in system.ts does: new Date(s.versionInfo!.updated!))
  2. Or parse all timestamps in the individual converters before the type assertion
🤖 Prompt for AI Agents
In src/lib/server/build-engine-api/types.ts around lines 53 to 65 and the later
SuccessResponse definition (lines ~86-87), the timestamp fields are typed as
Date but BuildEngine returns ISO timestamp strings; update SuccessResponse to
use optional string types for created/updated (matching StatusResponse) and then
update the converters (toProjectResponse, toJobResponse, toBuildResponse) to
explicitly parse those timestamp strings into Date objects where needed before
asserting to the response types (or, alternatively, keep types as strings
throughout and only construct Date objects at the application boundary) so type
declarations reflect the actual JSON and eliminate unsafe type assertions.

Comment on lines 84 to 87
responseType: 'project' | 'token' | 'job' | 'build' | 'release';
id: number;
created: string;
updated: string;
created: Date;
updated: Date;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, explore the file structure and find the types file
cd src/lib/server/build-engine-api && ls -la

Repository: sillsdev/appbuilder-portal

Length of output: 358


🏁 Script executed:

# Read the types.ts file to see the current definitions
cat -n src/lib/server/build-engine-api/types.ts | head -120 | tail -50

Repository: sillsdev/appbuilder-portal

Length of output: 1653


🏁 Script executed:

# Find converter functions that might handle response types
fd -t f '\.(ts|tsx)$' src/lib/server/build-engine-api/ | head -20

Repository: sillsdev/appbuilder-portal

Length of output: 195


🏁 Script executed:

# Search for converter functions and imports related to responses
rg -n 'to.*Response|toProject|toJob|toBuild' src/lib/server/build-engine-api/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 3146


🏁 Script executed:

# Check usages of created/updated fields across the codebase
rg -n '\.(created|updated)' src/lib/server/ --type ts -A 2 -B 2 | head -100

Repository: sillsdev/appbuilder-portal

Length of output: 407


🏁 Script executed:

# Look for any string type hints for created/updated fields
rg -n 'created.*:.*string|updated.*:.*string' src/lib/server/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 197


🏁 Script executed:

# See more of the types.ts file to understand the full type hierarchy
cat -n src/lib/server/build-engine-api/types.ts | head -100

Repository: sillsdev/appbuilder-portal

Length of output: 2889


🏁 Script executed:

# Check if there are any Date parsing utilities being used
rg -n 'new Date|Date\(' src/lib/server/build-engine-api/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Look at how the converters are actually being used - check if there's any post-processing
rg -n -A 5 'toProjectResponse\|toJobResponse\|toBuildResponse\|toTokenResponse\|toReleaseResponse' src/lib/server/build-engine-api/requests.ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check the actual API response parsing - look at how JSON is parsed
cat -n src/lib/server/build-engine-api/requests.ts | head -50

Repository: sillsdev/appbuilder-portal

Length of output: 2331


🏁 Script executed:

# Check if there's any middleware or interceptor that parses dates
rg -n 'reviver|parseDate|toDate' src/lib/server/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check if converters are being called with any transformation of the json parameter
cat -n src/lib/server/build-engine-api/types.ts | sed -n '118,210p'

Repository: sillsdev/appbuilder-portal

Length of output: 2820


🏁 Script executed:

# Look at the entire SuccessResponse and related response types
cat -n src/lib/server/build-engine-api/types.ts | sed -n '83,200p'

Repository: sillsdev/appbuilder-portal

Length of output: 3860


🏁 Script executed:

# Check if there's a global JSON reviver or date parsing in the fetch/request handling
rg -n 'JSON\.' src/lib/server/build-engine-api/requests.ts -A 2 -B 2

Repository: sillsdev/appbuilder-portal

Length of output: 236


🏁 Script executed:

# Search for usages of created/updated from the response types
rg -n 'ProjectResponse|JobResponse|BuildResponse|ReleaseResponse' src/lib/server/ --type ts | grep -v 'function\|export type\|type.*Response'

Repository: sillsdev/appbuilder-portal

Length of output: 2249


🏁 Script executed:

# Look for direct access to .created or .updated on response objects
rg -n '\.(created|updated)\s*[=\?\.;]' src/lib/server/ --type ts -B 3 | head -80

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check if there are any tests that might show the expected behavior
find . -name '*.test.ts' -o -name '*.spec.ts' | xargs grep -l 'created\|updated\|Response' 2>/dev/null | head -5

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check the actual git history to see if created/updated were changed from string to Date
git log --oneline --all -- src/lib/server/build-engine-api/types.ts | head -10

Repository: sillsdev/appbuilder-portal

Length of output: 211


🏁 Script executed:

# Check for any recent changes to see what was modified
git diff HEAD~5 HEAD -- src/lib/server/build-engine-api/types.ts 2>/dev/null || echo "Could not get diff"

Repository: sillsdev/appbuilder-portal

Length of output: 88


🏁 Script executed:

# Check actual usage of created/updated fields in the codebase
rg -n '\.created|\.updated' src/lib/server/job-executors/ src/lib/server/api/ --type ts -B 2 -A 2 | head -100

Repository: sillsdev/appbuilder-portal

Length of output: 484


🏁 Script executed:

# Look at the build.ts and publish.ts files that use BuildResponse and ReleaseResponse
grep -n 'created\|updated' src/lib/server/job-executors/build.ts src/lib/server/job-executors/publish.ts

Repository: sillsdev/appbuilder-portal

Length of output: 142


🏁 Script executed:

# Check if there are any Date methods being called on created/updated
rg -n 'created\.|updated\.' src/lib/server/ --type ts -B 1 -A 1

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Look at the actual requests.ts to understand the complete flow
cat -n src/lib/server/build-engine-api/requests.ts | sed -n '145,170p'

Repository: sillsdev/appbuilder-portal

Length of output: 1381


🏁 Script executed:

# Check the entire requests.ts file to understand the flow better
cat -n src/lib/server/build-engine-api/requests.ts | sed -n '1,80p'

Repository: sillsdev/appbuilder-portal

Length of output: 3402


🏁 Script executed:

# Look for any TypeScript configuration or middleware that might auto-convert dates
find . -name 'tsconfig.json' -o -name '.eslintrc*' | xargs cat 2>/dev/null | head -50

Repository: sillsdev/appbuilder-portal

Length of output: 755


🏁 Script executed:

# Check if there's a custom fetch or JSON parse implementation
grep -n 'res.json\|JSON.parse\|reviver' src/lib/server/build-engine-api/requests.ts

Repository: sillsdev/appbuilder-portal

Length of output: 1484


🏁 Script executed:

# Look at how the API responses are actually structured - check if there's documentation
find . -name '*README*' -o -name '*BUILD*ENGINE*' 2>/dev/null | head -5

Repository: sillsdev/appbuilder-portal

Length of output: 81


🏁 Script executed:

# Check if there's a custom reviver function or any date parsing middleware
grep -rn 'reviver\|dateParser\|parseDate' src/lib/server/build-engine-api/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 187


🏁 Script executed:

# Look at the complete types.ts to see if there's any transformation logic
tail -20 src/lib/server/build-engine-api/types.ts

Repository: sillsdev/appbuilder-portal

Length of output: 655


🏁 Script executed:

# Check how the API responses are actually used - look for type casting issues
rg -n 'as ProjectResponse\|as JobResponse\|as BuildResponse' src/lib/server/build-engine-api/types.ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check if StatusResponse's created/updated (which are strings) might be related
cat -n src/lib/server/build-engine-api/types.ts | sed -n '53,72p'

Repository: sillsdev/appbuilder-portal

Length of output: 740


🏁 Script executed:

# Let's verify the actual type mismatch by checking what res.json() would return
# Look for any response examples or test data
find . -name '*mock*' -o -name '*fixture*' 2>/dev/null | grep -i 'build\|api' | head -5

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Search for actual usage where created/updated are being accessed
rg -n '\.created\s*\(' src/lib/server/ --type ts -B 3 -A 3

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Let's look at the complete flow - check if there's any date reviver in fetch wrapper
cat -n src/lib/server/build-engine-api/requests.ts | sed -n '56,90p'

Repository: sillsdev/appbuilder-portal

Length of output: 1308


🏁 Script executed:

# Verify what types are actually returned at runtime - check for any JSON reviver
rg -n 'r\.json\(\)' src/lib/server/build-engine-api/requests.ts -B 5 -A 5

Repository: sillsdev/appbuilder-portal

Length of output: 415


🏁 Script executed:

# Search across the whole codebase for any custom JSON parsing with reviver
rg -n 'JSON\.parse.*reviver|reviver.*JSON' src/lib/server/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check if there's any intermediate transformation layer
rg -n 'function.*reviver|reviver.*=' src/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Final check: see if the actual JSON being returned is expected to be a string or object
cat -n src/lib/server/build-engine-api/requests.ts | sed -n '64,90p'

Repository: sillsdev/appbuilder-portal

Length of output: 963


🏁 Script executed:

# Check if there's TypeScript strict mode enabled and if this type mismatch would be caught
grep -n 'strict' tsconfig.json

Repository: sillsdev/appbuilder-portal

Length of output: 91


🏁 Script executed:

# Verify the actual runtime behavior - check if created/updated are being used as strings or Date
rg -n 'created\|updated' src/lib/server/job-executors/build.ts -B 3 -A 3 | head -50

Repository: sillsdev/appbuilder-portal

Length of output: 52


🏁 Script executed:

# Check what the actual API response structure is - look at publish.ts
cat -n src/lib/server/job-executors/publish.ts | grep -A 5 -B 5 'ReleaseResponse'

Repository: sillsdev/appbuilder-portal

Length of output: 500


🏁 Script executed:

# Check if TypeScript compilation would catch this mismatch
# Look at the actual type that would be inferred from the converter
cat -n src/lib/server/build-engine-api/types.ts | sed -n '118,120p'

Repository: sillsdev/appbuilder-portal

Length of output: 242


🏁 Script executed:

# Verify: when res.json() is awaited, what is the actual type?
# The json parameter in toProjectResponse comes from Record<string, unknown>
# Let's verify this is the actual issue
rg -n 'await res\.json' src/lib/server/build-engine-api/requests.ts -B 2 -A 2

Repository: sillsdev/appbuilder-portal

Length of output: 3669


🏁 Script executed:

# Final verification: Check if there's any place where created/updated fields are actually converted to Date
rg -n 'new Date.*created\|new Date.*updated' src/lib/server/ --type ts

Repository: sillsdev/appbuilder-portal

Length of output: 52


Type mismatch: created and updated typed as Date but not parsed from API responses.

The type definition declares created and updated as Date objects, but the converter functions (toProjectResponse, toJobResponse, etc.) do not parse these fields from strings. The API response contains date strings, which are spread directly into the response object without conversion. This creates a type safety issue where the TypeScript types don't match runtime values.

Update the converter functions to parse date strings to Date objects:

export function toProjectResponse(json: Record<string, unknown>): ProjectResponse {
  return {
    ...json,
    responseType: 'project',
    created: new Date(json.created as string),
    updated: new Date(json.updated as string)
  } as ProjectResponse;
}

Apply similar changes to toJobResponse, toBuildResponse, and toReleaseResponse.

🤖 Prompt for AI Agents
In src/lib/server/build-engine-api/types.ts around lines 84 to 87, the
Project/Job/Build/Release response types declare created and updated as Date but
the converter functions return the raw API JSON (strings), causing a type
mismatch; update each converter (toProjectResponse, toJobResponse,
toBuildResponse, toReleaseResponse) to explicitly parse created and updated from
the input JSON into Date objects (e.g., new Date(json.created as string) and new
Date(json.updated as string)) while keeping the rest of the spread properties
and responseType assignment so runtime values match the declared types.

Comment on lines 144 to 155
s.versionInfo?.versions
? Object.entries(s.versionInfo.versions)
.filter(([key]) => applications.get(key) && s.url)
.map(([Name, Version]) => ({
BuildEngineUrl: s.url!,
ApplicationTypeId: applications.get(Name)!,
Name,
Version,
ImageHash: s.versionInfo!.imageHash
DateUpdated: new Date(s.versionInfo!.updated!)
}))
: []
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unsafe non-null assertion could cause runtime error.

Line 144 uses optional chaining to check s.versionInfo?.versions, but line 152 assumes s.versionInfo.updated exists with a double non-null assertion (s.versionInfo!.updated!). If versions exists but updated is undefined, this will cause a runtime error.

Apply this diff to add proper null checking:

 const versionInfo = statuses.flatMap((s) =>
   s.versionInfo?.versions
     ? Object.entries(s.versionInfo.versions)
         .filter(([key]) => applications.get(key) && s.url)
         .map(([Name, Version]) => ({
           BuildEngineUrl: s.url!,
           ApplicationTypeId: applications.get(Name)!,
           Name,
           Version,
-          DateUpdated: new Date(s.versionInfo!.updated!)
+          DateUpdated: s.versionInfo.updated ? new Date(s.versionInfo.updated) : new Date()
         }))
     : []
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
s.versionInfo?.versions
? Object.entries(s.versionInfo.versions)
.filter(([key]) => applications.get(key) && s.url)
.map(([Name, Version]) => ({
BuildEngineUrl: s.url!,
ApplicationTypeId: applications.get(Name)!,
Name,
Version,
ImageHash: s.versionInfo!.imageHash
DateUpdated: new Date(s.versionInfo!.updated!)
}))
: []
);
s.versionInfo?.versions
? Object.entries(s.versionInfo.versions)
.filter(([key]) => applications.get(key) && s.url)
.map(([Name, Version]) => ({
BuildEngineUrl: s.url!,
ApplicationTypeId: applications.get(Name)!,
Name,
Version,
DateUpdated: s.versionInfo.updated ? new Date(s.versionInfo.updated) : new Date()
}))
: []
);
🤖 Prompt for AI Agents
In src/lib/server/job-executors/system.ts around lines 144 to 155, the code
checks s.versionInfo?.versions but then unconditionally uses
s.versionInfo!.updated! which can throw if updated is undefined; change the
logic to verify s.versionInfo.updated exists before mapping (e.g., filter
entries to require s.versionInfo?.updated != null), and when constructing
DateUpdated use safe access with a fallback (such as new Date() or skip the
record) instead of the double non-null assertion; also avoid asserting s.url and
rely on the earlier filter or add an explicit check to keep it safe.

Comment on lines 24 to 54
const computeTypes = ['small', 'medium'] as const;
type ComputeType = (typeof computeTypes)[number];
let computeType: ComputeType | null = $state(null);
$effect(() => {
computeType = getComputeType(value);
const parsed = JSON.parse(value || '{}');
computeType = (parsed['environment']?.['BUILD_COMPUTE_TYPE'] as ComputeType) ?? null;
});
function updateComputeType(type?: ComputeType) {
const toAdd = type
? { BUILD_COMPUTE_TYPE: type }
: {
// default value
BUILD_COMPUTE_TYPE: 'small',
BUILD_IMAGE_TAG: 'latest'
};
if (ok) {
const parsed = JSON.parse(value || '{}');
if (parsed['environment']) {
parsed['environment'] = {
...parsed['environment'],
...toAdd
};
} else {
parsed['environment'] = { ...toAdd };
}
value = JSON.stringify(parsed, null, 4);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard JSON parsing in the effect to avoid crashes on invalid JSON

$effect currently does JSON.parse(value || '{}') unconditionally. If PropertiesEditor updates value while the JSON is transiently invalid (very likely while the user is typing), this will throw and can break the modal, whereas updateComputeType is already safely gated on ok.

To make this resilient and also ignore unexpected BUILD_COMPUTE_TYPE values, consider:

  • Bailing early when ok is false.
  • Wrapping JSON.parse in a try/catch.
  • Only accepting BUILD_COMPUTE_TYPE values that are in computeTypes.

Example:

-  $effect(() => {
-    const parsed = JSON.parse(value || '{}');
-    computeType = (parsed['environment']?.['BUILD_COMPUTE_TYPE'] as ComputeType) ?? null;
-  });
+  $effect(() => {
+    if (!ok) {
+      // Avoid parsing while the JSON is known-invalid
+      return;
+    }
+
+    try {
+      const parsed = JSON.parse(value || '{}');
+      const envType = parsed['environment']?.['BUILD_COMPUTE_TYPE'] as string | undefined;
+
+      computeType = computeTypes.includes(envType as ComputeType)
+        ? (envType as ComputeType)
+        : null;
+    } catch {
+      // Defensive fallback if parsing ever fails
+      computeType = null;
+    }
+  });

This keeps the dropdown in sync with valid properties while preventing runtime exceptions when the JSON is in an invalid intermediate state.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const computeTypes = ['small', 'medium'] as const;
type ComputeType = (typeof computeTypes)[number];
let computeType: ComputeType | null = $state(null);
$effect(() => {
computeType = getComputeType(value);
const parsed = JSON.parse(value || '{}');
computeType = (parsed['environment']?.['BUILD_COMPUTE_TYPE'] as ComputeType) ?? null;
});
function updateComputeType(type?: ComputeType) {
const toAdd = type
? { BUILD_COMPUTE_TYPE: type }
: {
// default value
BUILD_COMPUTE_TYPE: 'small',
BUILD_IMAGE_TAG: 'latest'
};
if (ok) {
const parsed = JSON.parse(value || '{}');
if (parsed['environment']) {
parsed['environment'] = {
...parsed['environment'],
...toAdd
};
} else {
parsed['environment'] = { ...toAdd };
}
value = JSON.stringify(parsed, null, 4);
}
}
const computeTypes = ['small', 'medium'] as const;
type ComputeType = (typeof computeTypes)[number];
let computeType: ComputeType | null = $state(null);
$effect(() => {
if (!ok) {
// Avoid parsing while the JSON is known-invalid
return;
}
try {
const parsed = JSON.parse(value || '{}');
const envType = parsed['environment']?.['BUILD_COMPUTE_TYPE'] as string | undefined;
computeType = computeTypes.includes(envType as ComputeType)
? (envType as ComputeType)
: null;
} catch {
// Defensive fallback if parsing ever fails
computeType = null;
}
});
function updateComputeType(type?: ComputeType) {
const toAdd = type
? { BUILD_COMPUTE_TYPE: type }
: {
// default value
BUILD_COMPUTE_TYPE: 'small',
BUILD_IMAGE_TAG: 'latest'
};
if (ok) {
const parsed = JSON.parse(value || '{}');
if (parsed['environment']) {
parsed['environment'] = {
...parsed['environment'],
...toAdd
};
} else {
parsed['environment'] = { ...toAdd };
}
value = JSON.stringify(parsed, null, 4);
}
}
🤖 Prompt for AI Agents
In src/routes/(authenticated)/projects/[id=idNumber]/modals/Properties.svelte
around lines 24 to 54, the $effect unconditionally calls JSON.parse and can
throw on transient invalid JSON and accepts any BUILD_COMPUTE_TYPE value; change
the effect to first return early when ok is false, then wrap JSON.parse in a
try/catch to silently ignore invalid JSON, and when parsed successfully only set
computeType if parsed.environment?.BUILD_COMPUTE_TYPE is one of the allowed
computeTypes (otherwise set computeType to null).

…here, need to track why I am getting an internal error when trying to visit the page.
…gic to call doProductAction. Unable to determine where to store the comment made by user however.
…dded an optional comment parameter to doProductAction (and to a sub function Workflow.create) which is implemented for both starting a workflow and ending a workflow.
…how what organizations would be affected by the button.
…fy what rebuilds to call. Added initial logic to begin determinig whether to show start or pause on builds, but need more informaiton on what transitions look like in the database to make a determination.
… only affect projects not currently up to date with with SoftwareVersion. Rewrote some code for simplicity and readability.
…oductes for whatever organizations are currently being shown. Now to find products it will: get a list of products where: the product.project.organizationID is included in a list called searchOrgs, the project has not been archived, the project's setting for rebuildOnSoftwareUpdate is true, and the product's most recent productBuild.appbuilderversion is not what SystemVersions.version is when SystemVersion.BuildEngineURL equals the product.project.organization.buildengineurl and SystemVersion.ApplicationTypeId equals the project's applicationTypeId.
@becca-perk becca-perk force-pushed the feature/software-update-rebuild branch from 2cb79b0 to 550a6a0 Compare December 15, 2025 15:52
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (2)

98-127: Optimize N+1 query pattern.

Lines 104-114 execute a separate systemVersions.findUnique query for each product, creating an N+1 performance problem that will degrade significantly as the number of eligible products grows.

Fetch all required SystemVersions in a single query upfront, then perform in-memory lookups:

  const productsForRebuild: ProductToRebuild[] = [];

+  // Fetch all unique SystemVersions needed
+  const uniqueKeys = Array.from(
+    new Set(
+      eligibleProducts.map(
+        (p) => `${p.Project.Organization.BuildEngineUrl ?? ''}|${p.Project.TypeId}`
+      )
+    )
+  );
+  
+  const systemVersionsRaw = await DatabaseReads.systemVersions.findMany({
+    where: {
+      OR: uniqueKeys.map((key) => {
+        const [buildEngineUrl, typeIdStr] = key.split('|');
+        return {
+          BuildEngineUrl: buildEngineUrl,
+          ApplicationTypeId: Number(typeIdStr)
+        };
+      })
+    },
+    select: {
+      BuildEngineUrl: true,
+      ApplicationTypeId: true,
+      Version: true
+    }
+  });
+
+  // Create lookup map
+  const systemVersionsMap = new Map(
+    systemVersionsRaw.map((sv) => 
+      [`${sv.BuildEngineUrl}|${sv.ApplicationTypeId}`, sv.Version]
+    )
+  );

  // 2. Iterate through eligible products to perform the cross-model version check.
  for (const product of eligibleProducts) {
    const latestProductBuild = product.ProductBuilds[0];
    const latestVersion = latestProductBuild?.AppBuilderVersion ?? null;

-    // Get the required SystemVersion for this specific project's type and organization's build engine URL.
-    const requiredSystemVersion = await DatabaseReads.systemVersions.findUnique({
-      where: {
-        BuildEngineUrl_ApplicationTypeId: {
-          BuildEngineUrl: product.Project.Organization.BuildEngineUrl ?? '',
-          ApplicationTypeId: product.Project.TypeId
-        }
-      },
-      select: {
-        Version: true
-      }
-    });
-
-    const requiredVersion = requiredSystemVersion?.Version ?? null;
+    // Look up the required SystemVersion from the map
+    const key = `${product.Project.Organization.BuildEngineUrl ?? ''}|${product.Project.TypeId}`;
+    const requiredVersion = systemVersionsMap.get(key) ?? null;

    // 3. Apply the final filtering logic:

140-141: Fix inverted security check logic.

The security check conditions are backwards:

  • Line 140: When params.orgId exists (truthy), it calls requireAdminOfOrgIn(searchOrgs) checking admin rights for all orgs
  • Line 141: When params.orgId is missing, it calls requireAdminOfOrg(Number(params.orgId)) which evaluates to requireAdminOfOrg(NaN) and will fail

This is the opposite of the correct logic in the start action (lines 174-175).

Apply this diff to match the correct logic in the start action:

  // Determine what organizations are being affected
  const searchOrgs = await getOrganizations(locals, params);
-  if (Number(params.orgId)) locals.security.requireAdminOfOrgIn(searchOrgs);
-  else locals.security.requireAdminOfOrg(Number(params.orgId));
+  if (isNaN(Number(params.orgId))) locals.security.requireAdminOfOrgIn(searchOrgs);
+  else locals.security.requireAdminOfOrg(Number(params.orgId));
🧹 Nitpick comments (1)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (1)

28-36: Remove redundant RoleId.SuperAdmin from the filter.

Line 31 includes RoleId.SuperAdmin in the role filter, but super admins have already returned early at line 21-26. This filter will only execute for non-super-admin users, making the SuperAdmin inclusion redundant.

Apply this diff:

  const roles = await DatabaseReads.userRoles.findMany({
    where: {
      UserId: locals.security.userId,
-      RoleId: { in: [RoleId.SuperAdmin, RoleId.OrgAdmin] }
+      RoleId: RoleId.OrgAdmin
    },
    select: { OrganizationId: true }
  });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb79b0 and 550a6a0.

📒 Files selected for processing (8)
  • src/lib/locales/en-US.json (1 hunks)
  • src/lib/locales/es-419.json (1 hunks)
  • src/lib/locales/fr-FR.json (1 hunks)
  • src/lib/products/server.ts (3 hunks)
  • src/lib/server/workflow/index.ts (2 hunks)
  • src/routes/(authenticated)/+layout.svelte (1 hunks)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (1 hunks)
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/lib/products/server.ts
  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.svelte
  • src/lib/locales/fr-FR.json
  • src/lib/server/workflow/index.ts
  • src/routes/(authenticated)/+layout.svelte
  • src/lib/locales/es-419.json
🧰 Additional context used
🧠 Learnings (14)
📚 Learning: 2025-09-12T14:07:02.200Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:285-303
Timestamp: 2025-09-12T14:07:02.200Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to optimize filterAvailableActions by creating Sets at the caller level rather than inside the function, so the function would take Set<number> arguments instead of arrays for better performance and separation of concerns.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-22T14:59:53.366Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts:214-221
Timestamp: 2025-09-22T14:59:53.366Z
Learning: In the AppBuilder portal, bulk project actions in the route `/projects/[filter=projectSelector]/[orgId=idNumber]/+page.server.ts` are organization-scoped by design. The route parameter `[orgId=idNumber]` ensures all operations happen within a single organization context, so using the route-level orgId for all projects in bulk actions like `doProjectAction` is correct and safe.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-12T14:57:08.647Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/routes/(authenticated)/tasks/[product_id]/+page.server.ts:252-264
Timestamp: 2025-09-12T14:57:08.647Z
Learning: In src/routes/(authenticated)/tasks/[product_id]/+page.server.ts, FyreByrd prefers to use transition.target[0] prediction mechanism for determining post-send state rather than calling flow.state() after flow.send() due to race condition concerns. The prediction approach is sufficient for redirect decisions between staying on task page vs going to projects page.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-12T14:31:23.906Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1283
File: src/lib/server/workflow/index.ts:52-74
Timestamp: 2025-09-12T14:31:23.906Z
Learning: In the appbuilder-portal codebase, optional chaining in database queries (like `check?.Project._count.Authors`) is primarily used to satisfy TypeScript's type checker rather than handling expected null cases. The `check` being null should realistically never happen in the normal application flow.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-22T15:00:43.129Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.server.ts:7-10
Timestamp: 2025-09-22T15:00:43.129Z
Learning: In user settings layouts, use the `adminOrgs` function from `$lib/users/server` to determine admin access rather than throwing 403 errors upfront. The `adminOrgs` function creates a filter that returns organizations where the subject user is a member AND the current user is an admin of that organization.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-25T19:44:14.845Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(authenticated)/projects/import/[id=idNumber]/+page.server.ts:24-26
Timestamp: 2025-09-25T19:44:14.845Z
Learning: The `requireHasRole(organizationId, roleId, orOrgAdmin)` method in the Security class allows organization admins to pass role checks when the third parameter `orOrgAdmin` is set to `true`, even if they don't have the specific role being checked for.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-17T21:07:18.139Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/lib/projects/index.ts:183-201
Timestamp: 2025-09-17T21:07:18.139Z
Learning: In the AppBuilder portal project, org admins must be in the project group to claim/take ownership of projects. This is enforced both in client-side logic (canClaimProject function) and server-side validation.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-10-01T15:58:37.578Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1317
File: src/routes/(authenticated)/users/+page.svelte:51-55
Timestamp: 2025-10-01T15:58:37.578Z
Learning: In the appbuilder-portal codebase, the $orgActive store (from $lib/stores) is designed to always contain a valid organization ID and should never be null. Code that syncs values to $orgActive should include guard clauses to prevent null assignments.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-10T13:46:43.497Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/auth.ts:240-245
Timestamp: 2025-09-10T13:46:43.497Z
Learning: In src/auth.ts, the products branch intentionally uses params.id as a string for database lookups, not as a number. This is the correct behavior and should not be changed to numeric parsing.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-10-02T01:36:08.124Z
Learnt from: 7dev7urandom
Repo: sillsdev/appbuilder-portal PR: 1318
File: src/lib/components/OrganizationDropdown.svelte:30-39
Timestamp: 2025-10-02T01:36:08.124Z
Learning: In the `src/lib/components/OrganizationDropdown.svelte` component, the `organizations` array will always contain at least one organization, so there's no need to handle the empty array case.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-04T14:26:59.326Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1227
File: src/lib/server/job-executors/product.ts:247-250
Timestamp: 2025-09-04T14:26:59.326Z
Learning: In src/lib/server/job-executors/product.ts, the createLocal function's catch block returns false instead of rethrowing errors. This was implemented intentionally to fix another issue, so any changes to this error handling should be carefully evaluated for downstream impacts.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-10T13:51:14.948Z
Learnt from: FyreByrd
Repo: sillsdev/appbuilder-portal PR: 1275
File: src/routes/(authenticated)/projects/[id=idNumber]/+page.svelte:191-193
Timestamp: 2025-09-10T13:51:14.948Z
Learning: In the appbuilder-portal codebase, the `productsToAdd` property in project data is guaranteed to always be an array (possibly empty), never undefined.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-25T03:51:13.814Z
Learnt from: 7dev7urandom
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(unauthenticated)/invitations/organization-membership/+page.server.ts:7-8
Timestamp: 2025-09-25T03:51:13.814Z
Learning: In the organization invitation flow in src/routes/(unauthenticated)/invitations/organization-membership/+page.server.ts, the checkInviteErrors function properly handles null/undefined invite tokens by accepting inviteToken?: string | null and having an explicit guard clause if (!inviteToken) return { error: 'not found' }, making the non-null assertion on searchParams.get('t')! safe since error handling is centralized.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
📚 Learning: 2025-09-25T03:51:13.814Z
Learnt from: 7dev7urandom
Repo: sillsdev/appbuilder-portal PR: 1297
File: src/routes/(unauthenticated)/invitations/organization-membership/+page.server.ts:7-8
Timestamp: 2025-09-25T03:51:13.814Z
Learning: In the organization invitation flow in src/routes/(unauthenticated)/invitations/organization-membership/+page.server.ts, the checkInviteErrors function handles validation of invite tokens including null/undefined cases, so non-null assertion on searchParams.get('t') is safe because error handling is centralized in checkInviteErrors.

Applied to files:

  • src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts
🧬 Code graph analysis (1)
src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (2)
src/lib/server/database/prisma.ts (1)
  • DatabaseReads (22-22)
src/lib/products/server.ts (1)
  • doProductAction (7-97)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: checks / build-and-test
🔇 Additional comments (3)
src/lib/locales/en-US.json (1)

559-567: LGTM!

The localization keys are well-structured and align with the new admin software update feature. The key names follow existing conventions and clearly indicate their purpose.

src/routes/(authenticated)/software-update/[[orgId=idNumber]]/+page.server.ts (2)

39-46: LGTM!

The function correctly handles both cases: when a specific organization ID is provided via route parameter, and when it's missing (in which case it determines all target organizations).


165-186: LGTM!

The action handler correctly validates the form, performs proper security checks (lines 174-175), fetches eligible products, and triggers rebuilds in parallel using Promise.all. The logic is sound and efficient.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants