Skip to content

Conversation

@szpolny
Copy link

@szpolny szpolny commented Dec 20, 2025

Hi, i created plugin for managing mods for Arma Reforger. The plugin is mainly vibe-coded (because of my poor php knowledge), so free to suggest any improvements.

Summary by CodeRabbit

  • New Features
    • Workshop Mods management interface for installing, removing, and managing Arma Reforger server mods
    • Browse and search the Arma Reforger Workshop with filtering capabilities
    • View comprehensive mod details including versions, subscribers, ratings, and download counts
    • One-click mod installation with automatic server configuration
    • Performance-optimized caching for mod details and search results

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Walkthrough

This pull request introduces a new Arma Reforger Workshop Plugin for Filament, enabling server administrators to manage and browse Arma Reforger workshop mods. The implementation includes a plugin manifest, localization strings, a core service with mod management and workshop integration, a Facade for easy access, and two Filament pages for browsing and managing installed mods.

Changes

Cohort / File(s) Summary
Documentation & Configuration
README.md, plugin.json
README documents plugin features, configuration, and workshop integration; plugin.json defines metadata, namespace, and UI panel registration
Localization
lang/en/arma-reforger-workshop.php
English translation array for Workshop Mods UI, including navigation, labels, actions, forms, modals, and notifications
Plugin Infrastructure
src/ArmaReforgerWorkshopPlugin.php, src/Facades/ArmaReforgerWorkshop.php
Main plugin class implementing Filament Plugin interface with page discovery; Facade providing static proxy access to ArmaReforgerWorkshopService with documented methods
Service Layer
src/Services/ArmaReforgerWorkshopService.php
Core service for Arma Reforger integration: detects server type, manages mod installation/removal in config.json, fetches mod details with 6-hour caching, browses workshop with 15-minute search caching, generates workshop URLs, and validates mod installation state
Filament Pages
src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php, src/Filament/Server/Pages/BrowseWorkshopPage.php
ArmaReforgerWorkshopPage: displays paginated table of installed mods with details, enables add/remove operations, and edit config access; BrowseWorkshopPage: tabular browse interface with search, pagination, install actions, and installed status indicators

Sequence Diagrams

sequenceDiagram
    participant Admin as Admin User
    participant Page as ArmaReforgerWorkshop<br/>Page
    participant Service as Workshop<br/>Service
    participant FileRepo as Daemon File<br/>Repository
    participant Workshop as Bohemia<br/>Workshop

    rect rgb(200, 220, 255)
    Note over Admin,Workshop: Add Mod Flow
    Admin->>Page: Click "Add Mod" + Submit Form
    Page->>Service: addMod(server, modId, name, version)
    Service->>FileRepo: get config.json
    FileRepo-->>Service: file contents
    Service->>Service: Parse & update mods array
    Service->>FileRepo: write updated config.json
    FileRepo-->>Service: success
    Service-->>Page: bool result
    Page->>Admin: Show success notification
    end

    rect rgb(200, 220, 255)
    Note over Admin,Workshop: Browse & View Details
    Admin->>Page: Browse Workshop
    Page->>Service: browseWorkshop(search, page)
    Service->>Workshop: fetch search results (15 min cache)
    Workshop-->>Service: HTML with embedded JSON
    Service->>Service: Parse assets & extract mod data
    Service-->>Page: LengthAwarePaginator [mods]
    Page->>Admin: Display mods with details
    
    Admin->>Page: View mod details
    Page->>Service: getModDetails(modId)
    alt Cache Hit (6 hours)
        Service-->>Page: cached details
    else Cache Miss
        Service->>Workshop: fetch mod page
        Workshop-->>Service: HTML + embedded data
        Service->>Service: Parse & cache
        Service-->>Page: details array
    end
    Page->>Admin: Display mod info
    end
Loading
sequenceDiagram
    participant Admin as Admin User
    participant BrowsePage as BrowseWorkshop<br/>Page
    participant Service as Workshop<br/>Service
    participant FileRepo as Daemon File<br/>Repository

    rect rgb(220, 200, 255)
    Note over Admin,FileRepo: Install from Browse
    Admin->>BrowsePage: Click Install on mod row
    BrowsePage->>BrowsePage: Show confirmation modal
    Admin->>BrowsePage: Confirm installation
    BrowsePage->>Service: addMod(server, modId, name, version)
    Service->>FileRepo: get & parse config.json
    Service->>FileRepo: write mod entry to config.json
    alt Success
        FileRepo-->>Service: ok
        Service-->>BrowsePage: true
        BrowsePage->>Admin: Success notification + disable install action
    else Failure
        FileRepo-->>Service: error
        Service-->>BrowsePage: false
        BrowsePage->>Admin: Danger notification
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • ArmaReforgerWorkshopService.php: Logic-dense service with external API integration, HTML parsing of Bohemia Workshop, caching strategy (6-hour mod details, 15-minute search), error handling for resilient parsing, and configuration file manipulation via DaemonFileRepository
  • BrowseWorkshopPage.php: Complex Filament page with multi-step workflows (search, pagination, install action with confirmation, server context), dynamic table columns, conditional action visibility, and exception handling
  • ArmaReforgerWorkshopPage.php: Substantial page logic combining data augmentation (local mods + external details), add/remove flows, form validation (16-char hex modId), pagination, and per-row actions with notifications
  • Integration points: Verify proper error handling and messaging across Filament pages, Service, and DaemonFileRepository interactions; confirm caching policy implementation matches documented values
  • Access control: Ensure canAccess checks properly validate tenant server and Arma Reforger server status across both pages

Poem

🐰 A workshop mod manager hops into Filament's gleam,
With browsing, installing, a server admin's dream—
Mods cached and cached (six hours, or fifteen for search),
From Bohemia's pages, details we lurch!
Config.json updates, no duplicates here,
Arma Reforger servers, now managed with cheer! 🎮✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.67% 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
Title check ✅ Passed The title clearly describes the main change: adding a plugin for managing Arma Reforger mods, which aligns with the primary purpose of this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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.

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 (6)
arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (2)

169-177: Consider using a generic error message instead of exposing exception details.

Displaying $exception->getMessage() directly to users could expose internal implementation details or sensitive information. Consider using a generic error message and logging the full exception for debugging.

🔎 Proposed fix
                     } catch (Exception $exception) {
                             report($exception);
 
                             Notification::make()
                                 ->title('Failed to add mod')
-                                ->body($exception->getMessage())
+                                ->body('Could not update the server configuration. Please try again.')
                                 ->danger()
                                 ->send();
                         }

156-168: Use localization strings instead of hardcoded text.

The localization file defines notification strings (notifications.mod_added, notifications.mod_added_body, etc.) that are not being used here. This duplicates text and makes translations harder to maintain.

🔎 Proposed fix
                         if ($success) {
                             Notification::make()
-                                ->title('Mod added')
-                                ->body("'{$record['name']}' has been added to your server configuration.")
+                                ->title(__('arma-reforger-workshop::arma-reforger-workshop.notifications.mod_added'))
+                                ->body(__('arma-reforger-workshop::arma-reforger-workshop.notifications.mod_added_body', ['name' => $record['name']]))
                                 ->success()
                                 ->send();
                         } else {
                             Notification::make()
-                                ->title('Failed to add mod')
-                                ->body('Could not update the server configuration.')
+                                ->title(__('arma-reforger-workshop::arma-reforger-workshop.notifications.failed_to_add'))
+                                ->body(__('arma-reforger-workshop::arma-reforger-workshop.notifications.config_update_failed'))
                                 ->danger()
                                 ->send();
                         }
arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (1)

76-90: Unused $search parameter.

The $search parameter is declared but never used. Consider either implementing client-side filtering on the installed mods or removing the parameter if Filament allows.

Additionally, getModDetails() is called for each installed mod. While results are cached for 6 hours, the initial load with many mods could be slow due to sequential HTTP requests.

🔎 Proposed fix to use the search parameter
 $mods = ArmaReforgerWorkshop::getInstalledMods($server, $fileRepository);

+// Filter by search if provided
+if (!empty($search)) {
+    $mods = collect($mods)->filter(function ($mod) use ($search) {
+        return str_contains(strtolower($mod['name']), strtolower($search))
+            || str_contains(strtolower($mod['modId']), strtolower($search));
+    })->values()->toArray();
+}
+
 // Enrich with workshop details
 $enrichedMods = collect($mods)->map(function ($mod) {
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (3)

92-97: Case-sensitive comparison inconsistency.

The duplicate check here uses case-sensitive comparison (=== $modId), but isModInstalled() (line 318) uses case-insensitive comparison via strtoupper(). While the UI normalizes to uppercase before calling addMod, this inconsistency could cause issues if the service is called directly with mixed-case IDs.

Consider normalizing the modId to uppercase within this method for consistency:

🔎 Proposed fix
+        $modId = strtoupper($modId);
+
         // Check if mod already exists
         foreach ($config['game']['mods'] as $existingMod) {
-            if (($existingMod['modId'] ?? '') === $modId) {
+            if (strtoupper($existingMod['modId'] ?? '') === $modId) {
                 return true; // Already installed
             }
         }

138-141: Same case-sensitivity inconsistency in removeMod.

The filter uses case-sensitive comparison. For consistency with isModInstalled(), consider using case-insensitive comparison:

🔎 Proposed fix
+        $modId = strtoupper($modId);
+
         $config['game']['mods'] = collect($config['game']['mods'])
-            ->filter(fn ($mod) => ($mod['modId'] ?? '') !== $modId)
+            ->filter(fn ($mod) => strtoupper($mod['modId'] ?? '') !== $modId)
             ->values()
             ->toArray();

170-214: Error results may be cached for 6 hours.

If an exception occurs or the HTTP request fails inside the cache()->remember() callback, the minimal fallback result ['modId' => $modId] will be cached for 6 hours, preventing retry until the cache expires.

Consider caching only successful results:

🔎 Proposed fix using flexible() or manual caching
 public function getModDetails(string $modId): array
 {
-    return cache()->remember("arma_reforger_mod:$modId", now()->addHours(6), function () use ($modId) {
+    $cacheKey = "arma_reforger_mod:$modId";
+    $cached = cache()->get($cacheKey);
+    if ($cached !== null) {
+        return $cached;
+    }
+
+    $result = (function () use ($modId) {
         try {
             $response = Http::timeout(10)
                 ->connectTimeout(5)
                 ->get(self::WORKSHOP_URL . '/' . $modId);

             if (!$response->successful()) {
                 return ['modId' => $modId];
             }
             // ... rest of parsing logic ...

             return array_filter($details, fn ($v) => $v !== null);
         } catch (Exception $exception) {
             report($exception);
-            return ['modId' => $modId];
+            return null; // Don't cache errors
         }
-    });
+    })();
+
+    if ($result !== null && count($result) > 1) {
+        cache()->put($cacheKey, $result, now()->addHours(6));
+        return $result;
+    }
+
+    return ['modId' => $modId];
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 771c62c and 9914044.

📒 Files selected for processing (8)
  • arma-reforger-workshop/README.md (1 hunks)
  • arma-reforger-workshop/lang/en/arma-reforger-workshop.php (1 hunks)
  • arma-reforger-workshop/plugin.json (1 hunks)
  • arma-reforger-workshop/src/ArmaReforgerWorkshopPlugin.php (1 hunks)
  • arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1 hunks)
  • arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (1 hunks)
  • arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (1 hunks)
  • arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (3)
arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)
  • ArmaReforgerWorkshop (23-29)
arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (9)
  • canAccess (38-48)
  • getNavigationLabel (50-53)
  • getModelLabel (55-58)
  • getPluralModelLabel (60-63)
  • getTitle (65-68)
  • table (73-173)
  • getHeaderActions (175-253)
  • ArmaReforgerWorkshopPage (27-285)
  • content (255-284)
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (5)
  • isArmaReforgerServer (14-22)
  • browseWorkshop (222-285)
  • getModWorkshopUrl (158-161)
  • isModInstalled (313-324)
  • addMod (73-122)
arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (1)
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (8)
  • ArmaReforgerWorkshopService (10-325)
  • isArmaReforgerServer (14-22)
  • getInstalledMods (29-55)
  • getModDetails (168-215)
  • getModWorkshopUrl (158-161)
  • removeMod (127-153)
  • addMod (73-122)
  • getConfigPath (60-68)
arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (1)
  • ArmaReforgerWorkshopService (10-325)
arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (3)
arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)
  • ArmaReforgerWorkshop (23-29)
arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (1)
  • content (255-284)
arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (1)
  • content (209-219)
🪛 markdownlint-cli2 (0.18.1)
arma-reforger-workshop/README.md

61-61: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🪛 PHPMD (2.15.0)
arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php

76-76: Avoid unused parameters such as '$search'. (undefined)

(UnusedFormalParameter)

arma-reforger-workshop/src/ArmaReforgerWorkshopPlugin.php

22-22: Avoid unused parameters such as '$panel'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (19)
arma-reforger-workshop/plugin.json (1)

1-17: LGTM!

The plugin manifest is well-structured with all required metadata fields properly defined.

arma-reforger-workshop/src/ArmaReforgerWorkshopPlugin.php (1)

15-22: LGTM!

The plugin registration correctly uses Filament's page discovery mechanism. The empty boot() method with unused $panel parameter is required by the Plugin interface contract, so the static analysis warning is a false positive.

arma-reforger-workshop/README.md (1)

1-61: LGTM!

The README provides clear and comprehensive documentation covering features, requirements, configuration, and caching behavior. The bold styling for the author section is a minor style choice flagged by markdownlint, but it's acceptable for this context.

arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php (3)

35-45: LGTM!

The access control correctly validates both tenant existence and server type before granting access.


70-83: LGTM!

The table configuration correctly integrates with the workshop service, using deferred loading and matching pagination to the service's per-page count.


195-219: LGTM!

Header actions and content structure are properly implemented with clear navigation between pages and an accessible link to the external workshop.

arma-reforger-workshop/src/Facades/ArmaReforgerWorkshop.php (1)

23-29: Facade implementation is correct with comprehensive PHPDoc annotations.

The @method annotations (lines 11-19) accurately document all service methods with proper type hints and return types. The getFacadeAccessor returns the service class name, which Laravel will auto-resolve from the container.

Verify that the ArmaReforgerWorkshopService binding is registered in the application's service provider (typically in a service provider's boot method) to ensure proper container resolution at runtime.

arma-reforger-workshop/src/Filament/Server/Pages/ArmaReforgerWorkshopPage.php (5)

38-48: LGTM!

The access control logic properly validates the tenant exists before checking if it's an Arma Reforger server, and combines with parent access checks.


103-128: LGTM!

Good column configuration with appropriate placeholders, icons, and toggleable visibility.


130-172: LGTM!

The remove action has proper confirmation, error handling, and user feedback via notifications.


175-253: LGTM!

Good validation for mod ID (16-character hex), proper error handling, and consistent uppercase normalization before storing.


255-284: LGTM!

Good defensive error handling in the content display, gracefully falling back to 'Unknown' on failure.

arma-reforger-workshop/src/Services/ArmaReforgerWorkshopService.php (7)

14-22: LGTM!

Clean implementation with proper null coalescing for missing features/tags.


29-55: LGTM!

Robust implementation with proper error handling and filtering of invalid entries.


60-68: LGTM!

Clean implementation with sensible default fallback.


158-161: LGTM!

Simple and correct URL construction.


222-285: LGTM!

Good implementation with appropriate timeouts and 15-minute cache TTL. The same caching-of-failures concern applies here but is less impactful due to the shorter cache duration.


290-308: LGTM!

Good defensive implementation with proper fallbacks for missing thumbnail data.


313-324: LGTM!

Correct case-insensitive comparison for mod ID matching.

Comment on lines +34 to +36
'mod_added_body' => '\':name\' has been added to your server configuration.',
'mod_removed' => 'Mod removed',
'mod_removed_body' => '\':name\' has been removed from your server configuration.',
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

Unnecessary escape sequences in single-quoted strings.

In PHP single-quoted strings, single quotes don't need backslash escaping unless followed by another backslash. The \' here will display literal backslashes. If the intent is to wrap the mod name in quotes, consider using double quotes inside the string.

🔎 Proposed fix
-        'mod_added_body' => '\':name\' has been added to your server configuration.',
+        'mod_added_body' => '":name" has been added to your server configuration.',
         'mod_removed' => 'Mod removed',
-        'mod_removed_body' => '\':name\' has been removed from your server configuration.',
+        'mod_removed_body' => '":name" has been removed from your server configuration.',
📝 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
'mod_added_body' => '\':name\' has been added to your server configuration.',
'mod_removed' => 'Mod removed',
'mod_removed_body' => '\':name\' has been removed from your server configuration.',
'mod_added_body' => '":name" has been added to your server configuration.',
'mod_removed' => 'Mod removed',
'mod_removed_body' => '":name" has been removed from your server configuration.',
🤖 Prompt for AI Agents
In arma-reforger-workshop/lang/en/arma-reforger-workshop.php around lines 34-36,
the strings use escaped single quotes like '\':name\'' which is
unnecessary/confusing; update those entries to either remove the backslashes and
use a double-quoted outer string with single quotes inside (e.g. "':name' has
been added..."/"':name' has been removed...") or simply include the name without
extra quotes (':name has been added...'/':name has been removed...') so the
displayed text does not contain literal backslashes.

Comment on lines +132 to +139
->visible(function (array $record) {
/** @var Server $server */
$server = Filament::getTenant();
/** @var DaemonFileRepository $fileRepository */
$fileRepository = app(DaemonFileRepository::class);

return !ArmaReforgerWorkshop::isModInstalled($server, $fileRepository, $record['modId']);
})
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

Performance issue: N+1 calls to check mod installation status.

The visible() callbacks for both install and installed actions call isModInstalled() for every row. Since isModInstalled() internally calls getInstalledMods() which reads from the daemon file repository, this results in 24+ remote calls per page load. Consider caching the installed mods list once and checking against it.

🔎 Suggested approach

Add a property to cache installed mod IDs and compute it once:

protected ?array $installedModIds = null;

protected function getInstalledModIds(): array
{
    if ($this->installedModIds === null) {
        /** @var Server $server */
        $server = Filament::getTenant();
        /** @var DaemonFileRepository $fileRepository */
        $fileRepository = app(DaemonFileRepository::class);
        
        $installedMods = ArmaReforgerWorkshop::getInstalledMods($server, $fileRepository);
        $this->installedModIds = array_map(
            fn($mod) => strtoupper($mod['modId']),
            $installedMods
        );
    }
    return $this->installedModIds;
}

Then update the visible callbacks:

 ->visible(function (array $record) {
-    /** @var Server $server */
-    $server = Filament::getTenant();
-    /** @var DaemonFileRepository $fileRepository */
-    $fileRepository = app(DaemonFileRepository::class);
-
-    return !ArmaReforgerWorkshop::isModInstalled($server, $fileRepository, $record['modId']);
+    return !in_array(strtoupper($record['modId']), $this->getInstalledModIds(), true);
 })

Also applies to: 184-191

🤖 Prompt for AI Agents
In arma-reforger-workshop/src/Filament/Server/Pages/BrowseWorkshopPage.php
around lines 132-139 and 184-191, the visible() callbacks call
ArmaReforgerWorkshop::isModInstalled() per-row causing N+1 remote reads; add a
protected ?array $installedModIds property and a getInstalledModIds() method
that on first call retrieves ArmaReforgerWorkshop::getInstalledMods($server,
$fileRepository), maps mod IDs (uppercased) into the property, then return it,
and update both visible() callbacks to check existence against
getInstalledModIds() (use the same casing normalization) instead of calling
isModInstalled() per row.

Copy link
Member

@Boy132 Boy132 left a comment

Choose a reason for hiding this comment

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

Please fix pint and what rabbit mentioned.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants