Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5bb223d
fix: resolve all lint issues and drupal-check configuration
Sep 10, 2025
d1695ab
fix: resolve drupal-check dependency conflicts with PHP 8.3
Sep 10, 2025
7392d01
fix: resolve all drupal-check errors and improve code quality
Sep 10, 2025
a4f4f8a
fix: make drupal-check failures visible instead of silent
Sep 10, 2025
e4b20cd
Remove weight from RL Experiments report menu item
Nov 3, 2025
cd687c5
fix drupal check version
Nov 3, 2025
6eb06ad
Update run-drupal-check.sh to use PHPStan instead of drupal-check
Nov 3, 2025
c969f32
docs: add Nginx configuration instructions for rl.php
Nov 4, 2025
154901b
feat: add arm data validation service to prevent calculation errors
Nov 4, 2025
f5e6ab6
refactor: integrate ArmDataValidator into ExperimentManager and Repor…
Nov 4, 2025
c6ee809
fix: add defensive guards in ThompsonCalculator against invalid inputs
Nov 4, 2025
8b1dcff
feat: add hook_requirements check for rl.php accessibility
Nov 4, 2025
2547fa3
Add reset tip to RL reports page and improve code quality
Nov 4, 2025
956f439
docs: add experiment decorator system documentation
Jan 26, 2026
dcf8783
Optimize Plotly charts for UX and fix tooltip issues
Jan 26, 2026
4af872d
fix(charts): resolve infinite resize loop and improve 3D chart rendering
Jan 26, 2026
043adac
refactor(reports): move inline CSS/HTML to proper Drupal files
Jan 26, 2026
3a92836
feat(reports): add translatable UI strings optimized for content mark…
Jan 26, 2026
d2934a7
feat(storage): add snapshot storage for chart historical data
Jan 26, 2026
5ef9136
chore(deps): add Plotly.js library for chart rendering
Jan 26, 2026
d28bed4
fix: resolve PHPStan and phpcs lint errors for CI compliance
Jan 26, 2026
9cbffc6
refactor: simplify charts to 2D line (≤10 arms) and 3D landscape (>10…
Jan 26, 2026
614f0e0
fix: address PR review feedback
Jan 26, 2026
e2b2de0
fix: standardize chart titles and move variant count to tip
Jan 26, 2026
8794b7f
feat: add Conversions column to experiments overview table
Jan 26, 2026
9770c0e
feat: add date filters, adaptive lighting, and address PR review
Jan 26, 2026
c7bc7a4
fix: remove duplicate date filter and clean up code
Jan 26, 2026
e20b5b3
fix: address PR review feedback
Jan 26, 2026
d1887da
refactor: DRY improvements to ReportsController
Jan 26, 2026
7b517b0
fix: use Asset Packagist for Plotly.js (not CDN)
Jan 26, 2026
6a91f45
refactor: DRY improvements to JavaScript (reviewer suggestions)
Jan 26, 2026
a192187
fix: move RL settings from Development to Content authoring section
Jan 27, 2026
6ff81c1
refactor: move settings to Web Services and use full name
Jan 27, 2026
c04397a
docs: improve snapshot table help text with technical accuracy
Jan 27, 2026
b9384d2
style: simplify 3D chart area background to solid color
Jan 27, 2026
93c8126
refactor(css): remove breakpoints, use relative units
Jan 27, 2026
f79353f
fix: ensure date filters visible on both 2D and 3D chart pages
Jan 27, 2026
8eed87d
feat: add confirmation page when disabling event logging
Jan 27, 2026
1a54f69
feat: enable event logging by default
Jan 27, 2026
05b1df9
fix: remove unused variable in DisableEventLogConfirmForm
Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 113 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,50 @@ drush en rl

### Post-Installation: Verify rl.php Access

The RL module includes a `.htaccess` file that allows direct access to `rl.php` (following the same pattern as Drupal 11's contrib statistics module). Test that it's working:
The RL module includes a `.htaccess` file that allows direct access to
`rl.php` (following the same pattern as Drupal 11's contrib statistics
module). Test that it's working:

```bash
# Test if rl.php is accessible
curl -X POST -d "action=turns&experiment_id=test&arm_ids=1" http://example.com/modules/contrib/rl/rl.php
curl -X POST -d "action=turns&experiment_id=test&arm_ids=1" \
http://example.com/modules/contrib/rl/rl.php
```

**If the test fails:**

- **Apache**: Ensure `.htaccess` files are processed (`AllowOverride All`)
- **Nginx**: Copy the rewrite rules from `.htaccess` to your server config
- **Nginx**: Add the configuration rules below to your server block
- **Security modules**: Whitelist `/modules/contrib/rl/rl.php`

If server policies prevent direct access to `rl.php`, use the Drupal Routes API instead.
#### Nginx Configuration

Add these rules to your Nginx server block, **before** the main Drupal location block:

```nginx
# Allow direct access to rl.php for performance
location ~ ^/modules/contrib/rl/rl\.php$ {
fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param QUERY_STRING $query_string;
fastcgi_pass unix:/var/run/php/php-fpm.sock; # Adjust to your PHP-FPM socket
}

# Block access to other PHP files in modules (except rl.php)
location ~ ^/modules/.*\.php$ {
deny all;
}
```

**Note:** Adjust `fastcgi_pass` to match your PHP-FPM configuration:
- Socket: `unix:/var/run/php/php8.1-fpm.sock` (or your PHP version)
- TCP: `127.0.0.1:9000`

If server policies prevent direct access to `rl.php`, use the Drupal
Routes API instead.

## API Usage

Expand Down Expand Up @@ -136,6 +166,85 @@ RL provides optional cache management for web components:
Full algorithm details available in source code:
[ThompsonCalculator.php](https://git.drupalcode.org/project/rl/-/blob/1.x/src/Service/ThompsonCalculator.php)

## Experiment Decorators

Decorators customize how experiments and arms are displayed in the RL reports
interface. By default, experiments and arms show their raw IDs, but decorators
can provide human-readable labels.

### Creating a Decorator

Implement the `ExperimentDecoratorInterface`:

```php
<?php

namespace Drupal\my_module\Decorator;

use Drupal\rl\Decorator\ExperimentDecoratorInterface;

class MyExperimentDecorator implements ExperimentDecoratorInterface {

/**
* {@inheritdoc}
*/
public function decorateExperiment(string $experiment_id): ?array {
// Return NULL to skip, or a render array for custom display.
if (!str_starts_with($experiment_id, 'my_module-')) {
return NULL;
}
return ['#markup' => 'My Custom Experiment Name'];
}

/**
* {@inheritdoc}
*/
public function decorateArm(string $experiment_id, string $arm_id): ?array {
// Return NULL to skip, or a render array for custom display.
if (!str_starts_with($experiment_id, 'my_module-')) {
return NULL;
}
// Example: Load entity and return its label.
$entity = \Drupal::entityTypeManager()->getStorage('node')->load($arm_id);
if ($entity) {
return [
'#markup' => htmlspecialchars($entity->label()) .
' <small>(' . htmlspecialchars($arm_id) . ')</small>',
];
}
return NULL;
}

}
```

### Registering the Decorator

Add the decorator service to your module's `*.services.yml` with the
`rl_experiment_decorator` tag:

```yaml
services:
my_module.experiment_decorator:
class: Drupal\my_module\Decorator\MyExperimentDecorator
arguments: ['@entity_type.manager']
tags:
- { name: rl_experiment_decorator }
```

The decorator manager automatically discovers all tagged services and calls
them in order until one returns a non-NULL value.

### Best Practices

- **Check experiment prefix**: Return `NULL` early for experiments your
decorator doesn't handle.
- **Handle missing entities**: Entities may be deleted; return `NULL` if the
entity can't be loaded.
- **Use render arrays**: Return proper Drupal render arrays for consistent
theming and security.
- **Escape output**: Use `htmlspecialchars()` for any user-provided content.

## Development

### Linting and Code Standards
Expand Down
14 changes: 14 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "drupal/rl",
"type": "drupal-module",
"description": "Reinforcement Learning module for Drupal using Thompson Sampling.",
"license": "GPL-2.0-or-later",
"require": {
"npm-asset/plotly.js-dist-min": "^2.35"
},
"extra": {
"installer-paths": {
"libraries/{$name}": ["type:npm-asset"]
}
}
}
3 changes: 3 additions & 0 deletions config/install/rl.settings.yml
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
debug_mode: false
enable_event_log: true
event_log_max_rows: 100000
chart_line_threshold: 10
9 changes: 9 additions & 0 deletions config/schema/rl.schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,12 @@ rl.settings:
debug_mode:
type: boolean
label: 'Debug mode'
enable_event_log:
type: boolean
label: 'Enable event log for historical visualization'
event_log_max_rows:
type: integer
label: 'Maximum rows in event log table'
chart_line_threshold:
type: integer
label: 'Maximum arms for 2D line chart'
131 changes: 131 additions & 0 deletions css/rl-charts.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* @file
* Styles for RL experiment charts.
*/

.rl-charts-container {
margin-bottom: 2em;
}

.rl-chart-row {
display: flex;
flex-wrap: wrap;
gap: 1.5em;
margin-bottom: 1.5em;
}

.rl-chart-box {
flex: 1 1 100%;
min-width: 0;
background: #f8f9fa;
border: 2px solid #d0d0d0;
border-radius: 8px;
padding: clamp(8px, 1.5vw, 20px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
position: relative;
}

.rl-chart-filters {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
margin-bottom: 1em;
position: relative;
z-index: 10;
}

/* Loading spinner for chart regeneration */
.rl-chart-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
border-radius: 8px;
}

.rl-chart-loading::after {
content: '';
width: 40px;
height: 40px;
border: 4px solid #e9ecef;
border-top-color: #0d6efd;
border-radius: 50%;
animation: rl-spin 0.8s linear infinite;
}

@keyframes rl-spin {
to {
transform: rotate(360deg);
}
}

.rl-chart-description {
color: #444;
margin-bottom: 1em;
line-height: 1.5;
}

.rl-chart-area-2d,
.rl-chart-area-3d {
min-height: 70vh;
max-height: 90vh;
width: 100%;
}

.rl-chart-tip {
position: relative;
z-index: 10;
}

/* Date filter styles */
.rl-date-filter {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75em;
}

.rl-presets,
.rl-axes {
display: flex;
align-items: center;
gap: 0.4em;
}

.rl-presets strong,
.rl-axes strong {
color: #555;
white-space: nowrap;
}

.rl-preset-select,
.rl-axis-select {
padding: 0.25em 0.5em;
background: #fff;
border: 1px solid #ced4da;
border-radius: 4px;
color: #495057;
cursor: pointer;
}

.rl-preset-select:hover,
.rl-axis-select:hover {
border-color: #adb5bd;
}

.rl-preset-select:focus,
.rl-axis-select:focus {
border-color: #0d6efd;
outline: none;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
}

.rl-range-info {
display: none;
}
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ services:
command: bash -c "/src/scripts/run-drupal-check.sh"
tty: true
environment:
DRUPAL_RECOMMENDED_PROJECT: 11.2.x-dev
DRUPAL_RECOMMENDED_PROJECT: 11.x-dev
volumes:
- .:/src
32 changes: 32 additions & 0 deletions js/rl-date-filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
(function (Drupal, once) {
'use strict';

Drupal.behaviors.rlDateFilter = {
attach: function (context) {
const selects = once('rl-date-filter', '.rl-filter-select', context);

selects.forEach(function (select) {
select.addEventListener('change', function () {
const urls = JSON.parse(this.dataset.urls);
const url = urls[this.value];

// Find visible chart boxes and add loading overlay
const chartBoxes = document.querySelectorAll('.rl-chart-box');
chartBoxes.forEach(function (chartBox) {
if (chartBox.offsetParent !== null) {
const loader = document.createElement('div');
loader.className = 'rl-chart-loading';
chartBox.appendChild(loader);
}
});

// Navigate after a brief delay to show the spinner
setTimeout(function () {
window.location.href = url;
}, 50);
});
});
}
};

})(Drupal, once);
Loading