fix: resolve CI linting issues for Astroglobe

Fix all ESLint, Stylelint, PHP CS Fixer, and Psalm workflow errors.

Changes:
- ESLint fixes:
  - Remove unused APP_NAME constant
  - Remove unused TextBoxOutline and TextBoxRemoveOutline components
  - Remove unused container variable in adminSettings.js
  - Auto-fix trailing commas, line breaks, attribute ordering
- PHP CS Fixer:
  - Add trailing commas after last function parameters
  - Convert double quotes to single quotes in log messages
  - Remove unused NoCSRFRequired import
  - Fix arrow function formatting
- Stylelint:
  - Update config to use @nextcloud/stylelint-config
  - Fix extends directive (was using non-existent package)
- Psalm workflow:
  - Fix jq object indexing (.include[0] instead of .[0])
  - Correctly extract OCP version from matrix output

All checks now pass locally.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-12-15 22:05:14 +01:00
parent 5a6205476a
commit dfc81923ba
15 changed files with 109 additions and 88 deletions
+1 -1
View File
@@ -247,7 +247,7 @@ jobs:
- name: Install OCP for static analysis
run: |
# Get first OCP version from matrix
OCP_VERSION=$(echo '${{ steps.ocp-versions.outputs.ocp-matrix }}' | jq -r '.[0]."ocp-version"')
OCP_VERSION=$(echo '${{ steps.ocp-versions.outputs.ocp-matrix }}' | jq -r '.include[0]."ocp-version"')
composer require --dev "nextcloud/ocp:$OCP_VERSION" --ignore-platform-reqs --with-dependencies
- name: Run Psalm
+4 -5
View File
@@ -12,7 +12,6 @@ use OCA\Astroglobe\Settings\Admin as AdminSettings;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\IConfig;
@@ -44,7 +43,7 @@ class ApiController extends Controller {
LoggerInterface $logger,
McpTokenStorage $tokenStorage,
IConfig $config,
IdpTokenRefresher $tokenRefresher
IdpTokenRefresher $tokenRefresher,
) {
parent::__construct($appName, $request);
$this->client = $client;
@@ -126,7 +125,7 @@ class ApiController extends Controller {
string $algorithm = 'hybrid',
int $limit = 10,
string $doc_types = '',
string $include_pca = 'true'
string $include_pca = 'true',
): JSONResponse {
if (empty($query)) {
return new JSONResponse([
@@ -185,7 +184,7 @@ class ApiController extends Controller {
$validDocTypes = ['note', 'file', 'deck_card', 'calendar', 'contact', 'news_item'];
$docTypesArray = array_filter(
explode(',', $doc_types),
fn($t) => in_array(trim($t), $validDocTypes)
fn ($t) => in_array(trim($t), $validDocTypes)
);
$docTypesArray = array_map('trim', $docTypesArray);
if (empty($docTypesArray)) {
@@ -678,7 +677,7 @@ class ApiController extends Controller {
string $doc_type,
string $doc_id,
int $start,
int $end
int $end,
): JSONResponse {
$user = $this->userSession->getUser();
if (!$user) {
+12 -12
View File
@@ -47,7 +47,7 @@ class OAuthController extends Controller {
McpTokenStorage $tokenStorage,
LoggerInterface $logger,
IL10N $l,
IClientService $clientService
IClientService $clientService,
) {
parent::__construct($appName, $request);
$this->config = $config;
@@ -73,11 +73,11 @@ class OAuthController extends Controller {
#[NoAdminRequired]
#[NoCSRFRequired]
public function initiateOAuth() {
$this->logger->info("initiateOAuth called");
$this->logger->info('initiateOAuth called');
$user = $this->userSession->getUser();
if (!$user) {
$this->logger->error("initiateOAuth: User not authenticated");
$this->logger->error('initiateOAuth: User not authenticated');
return new TemplateResponse(
'astroglobe',
'settings/error',
@@ -85,7 +85,7 @@ class OAuthController extends Controller {
);
}
$this->logger->info("initiateOAuth: User authenticated: " . $user->getUID());
$this->logger->info('initiateOAuth: User authenticated: ' . $user->getUID());
try {
// Get MCP server configuration
@@ -107,9 +107,9 @@ class OAuthController extends Controller {
$codeVerifier = bin2hex(random_bytes(32));
$codeChallenge = $this->base64UrlEncode(hash('sha256', $codeVerifier, true));
$this->logger->info("Using public client mode with PKCE");
$this->logger->info('Using public client mode with PKCE');
} else {
$this->logger->info("Using confidential client mode with client secret");
$this->logger->info('Using confidential client mode with client secret');
}
// Generate state for CSRF protection
@@ -129,7 +129,7 @@ class OAuthController extends Controller {
$codeChallenge
);
$this->logger->info("Initiating OAuth flow for user: " . $user->getUID());
$this->logger->info('Initiating OAuth flow for user: ' . $user->getUID());
return new RedirectResponse($authUrl);
} catch (\Exception $e) {
@@ -163,7 +163,7 @@ class OAuthController extends Controller {
string $code = '',
string $state = '',
?string $error = null,
?string $error_description = null
?string $error_description = null,
): RedirectResponse {
try {
// Check for errors from IdP
@@ -292,7 +292,7 @@ class OAuthController extends Controller {
private function buildAuthorizationUrl(
string $mcpServerUrl,
string $state,
?string $codeChallenge
?string $codeChallenge,
): string {
// First, query MCP server to discover which IdP it's configured to use
$this->logger->info('buildAuthorizationUrl: Starting', [
@@ -430,7 +430,7 @@ class OAuthController extends Controller {
private function exchangeCodeForToken(
string $mcpServerUrl,
string $code,
?string $codeVerifier
?string $codeVerifier,
): array {
// Query MCP server to discover which IdP it's configured to use
try {
@@ -496,11 +496,11 @@ class OAuthController extends Controller {
if (!empty($clientSecret)) {
// Confidential client: use client secret for authentication
$postData['client_secret'] = $clientSecret;
$this->logger->info("Using client secret for token exchange");
$this->logger->info('Using client secret for token exchange');
} elseif ($codeVerifier !== null) {
// Public client: use PKCE proof for authentication
$postData['code_verifier'] = $codeVerifier;
$this->logger->info("Using PKCE code verifier for token exchange");
$this->logger->info('Using PKCE code verifier for token exchange');
} else {
throw new \Exception('Neither client_secret nor code_verifier available for token exchange');
}
@@ -247,8 +247,8 @@ class SemanticSearchProvider implements IProvider {
: $this->urlGenerator->linkToRouteAbsolute('files.view.index'),
'deck_card' => isset($result['board_id']) && $id
? $this->urlGenerator->linkToRoute('deck.page.index') .
"board/{$result['board_id']}/card/{$id}"
? $this->urlGenerator->linkToRoute('deck.page.index')
. "board/{$result['board_id']}/card/{$id}"
: $this->urlGenerator->linkToRoute('deck.page.index'),
'calendar', 'calendar_event' => $this->urlGenerator->linkToRoute('calendar.view.index'),
+1 -1
View File
@@ -25,7 +25,7 @@ class IdpTokenRefresher {
public function __construct(
IConfig $config,
IClientService $clientService,
LoggerInterface $logger
LoggerInterface $logger,
) {
$this->config = $config;
$this->httpClient = $clientService->newClient();
+7 -7
View File
@@ -24,7 +24,7 @@ class McpServerClient {
public function __construct(
IClientService $clientService,
IConfig $config,
LoggerInterface $logger
LoggerInterface $logger,
) {
$this->httpClient = $clientService->newClient();
$this->config = $config;
@@ -85,7 +85,7 @@ class McpServerClient {
public function getUserSession(string $userId, string $token): array {
try {
$response = $this->httpClient->get(
$this->baseUrl . "/api/v1/users/" . urlencode($userId) . "/session",
$this->baseUrl . '/api/v1/users/' . urlencode($userId) . '/session',
[
'headers' => [
'Authorization' => 'Bearer ' . $token
@@ -120,7 +120,7 @@ class McpServerClient {
public function revokeUserAccess(string $userId, string $token): array {
try {
$response = $this->httpClient->post(
$this->baseUrl . "/api/v1/users/" . urlencode($userId) . "/revoke",
$this->baseUrl . '/api/v1/users/' . urlencode($userId) . '/revoke',
[
'headers' => [
'Authorization' => 'Bearer ' . $token
@@ -203,7 +203,7 @@ class McpServerClient {
int $limit = 10,
bool $includePca = true,
?array $docTypes = null,
?string $token = null
?string $token = null,
): array {
try {
$requestBody = [
@@ -284,7 +284,7 @@ class McpServerClient {
int $offset = 0,
string $algorithm = 'hybrid',
string $fusion = 'rrf',
float $scoreThreshold = 0.0
float $scoreThreshold = 0.0,
): array {
try {
$response = $this->httpClient->post(
@@ -416,7 +416,7 @@ class McpServerClient {
string $event,
string $uri,
?array $eventFilter,
string $token
string $token,
): array {
try {
$requestBody = [
@@ -549,7 +549,7 @@ class McpServerClient {
string $docId,
int $start,
int $end,
string $token
string $token,
): array {
try {
$response = $this->httpClient->get(
+3 -3
View File
@@ -22,7 +22,7 @@ class McpTokenStorage {
public function __construct(
IConfig $config,
ICrypto $crypto,
LoggerInterface $logger
LoggerInterface $logger,
) {
$this->config = $config;
$this->crypto = $crypto;
@@ -43,7 +43,7 @@ class McpTokenStorage {
string $userId,
string $accessToken,
string $refreshToken,
int $expiresAt
int $expiresAt,
): void {
try {
$tokenData = [
@@ -158,7 +158,7 @@ class McpTokenStorage {
*
* @param string $userId User ID
* @param callable|null $refreshCallback Callback to refresh token if expired
* Should accept (refreshToken) and return new token data
* Should accept (refreshToken) and return new token data
* @return string|null Access token, or null if not available
*/
public function getAccessToken(string $userId, ?callable $refreshCallback = null): ?string {
+11 -11
View File
@@ -12,24 +12,24 @@ namespace OCA\Astroglobe\Service;
*/
class WebhookPresets {
// File/Notes webhook events
public const FILE_EVENT_CREATED = "OCP\\Files\\Events\\Node\\NodeCreatedEvent";
public const FILE_EVENT_WRITTEN = "OCP\\Files\\Events\\Node\\NodeWrittenEvent";
public const FILE_EVENT_CREATED = 'OCP\\Files\\Events\\Node\\NodeCreatedEvent';
public const FILE_EVENT_WRITTEN = 'OCP\\Files\\Events\\Node\\NodeWrittenEvent';
// Use BeforeNodeDeletedEvent instead of NodeDeletedEvent to get node.id
// See: https://github.com/nextcloud/server/issues/56371
public const FILE_EVENT_DELETED = "OCP\\Files\\Events\\Node\\BeforeNodeDeletedEvent";
public const FILE_EVENT_DELETED = 'OCP\\Files\\Events\\Node\\BeforeNodeDeletedEvent';
// Calendar webhook events
public const CALENDAR_EVENT_CREATED = "OCP\\Calendar\\Events\\CalendarObjectCreatedEvent";
public const CALENDAR_EVENT_UPDATED = "OCP\\Calendar\\Events\\CalendarObjectUpdatedEvent";
public const CALENDAR_EVENT_DELETED = "OCP\\Calendar\\Events\\CalendarObjectDeletedEvent";
public const CALENDAR_EVENT_CREATED = 'OCP\\Calendar\\Events\\CalendarObjectCreatedEvent';
public const CALENDAR_EVENT_UPDATED = 'OCP\\Calendar\\Events\\CalendarObjectUpdatedEvent';
public const CALENDAR_EVENT_DELETED = 'OCP\\Calendar\\Events\\CalendarObjectDeletedEvent';
// Tables webhook events (Nextcloud 30+)
public const TABLES_EVENT_ROW_ADDED = "OCA\\Tables\\Event\\RowAddedEvent";
public const TABLES_EVENT_ROW_UPDATED = "OCA\\Tables\\Event\\RowUpdatedEvent";
public const TABLES_EVENT_ROW_DELETED = "OCA\\Tables\\Event\\RowDeletedEvent";
public const TABLES_EVENT_ROW_ADDED = 'OCA\\Tables\\Event\\RowAddedEvent';
public const TABLES_EVENT_ROW_UPDATED = 'OCA\\Tables\\Event\\RowUpdatedEvent';
public const TABLES_EVENT_ROW_DELETED = 'OCA\\Tables\\Event\\RowDeletedEvent';
// Forms webhook events (Nextcloud 30+)
public const FORMS_EVENT_FORM_SUBMITTED = "OCA\\Forms\\Events\\FormSubmittedEvent";
public const FORMS_EVENT_FORM_SUBMITTED = 'OCA\\Forms\\Events\\FormSubmittedEvent';
// NOTE: Deck and Contacts do NOT support webhooks
// Their event classes do not implement IWebhookCompatibleEvent interface.
@@ -163,7 +163,7 @@ class WebhookPresets {
}
return array_map(
fn($eventConfig) => $eventConfig['event'],
fn ($eventConfig) => $eventConfig['event'],
$preset['events']
);
}
+1 -1
View File
@@ -36,7 +36,7 @@ class Admin implements ISettings {
public function __construct(
McpServerClient $client,
IConfig $config,
IInitialState $initialState
IInitialState $initialState,
) {
$this->client = $client;
$this->config = $config;
+1 -1
View File
@@ -33,7 +33,7 @@ class Personal implements ISettings {
IUserSession $userSession,
IInitialState $initialState,
McpTokenStorage $tokenStorage,
IURLGenerator $urlGenerator
IURLGenerator $urlGenerator,
) {
$this->client = $client;
$this->userSession = $userSession;
+35 -22
View File
@@ -118,7 +118,7 @@
min="0"
max="100"
step="5"
class="mcp-score-slider" />
class="mcp-score-slider">
</div>
</div>
</div>
@@ -248,29 +248,43 @@
<div v-else-if="vectorStatus" class="mcp-status-cards">
<div class="mcp-status-card">
<div class="mcp-status-label">{{ t('astroglobe', 'Sync Status') }}</div>
<div class="mcp-status-label">
{{ t('astroglobe', 'Sync Status') }}
</div>
<div class="mcp-status-value" :class="'status-' + vectorStatus.status">
{{ vectorStatus.status }}
</div>
</div>
<div class="mcp-status-card">
<div class="mcp-status-label">{{ t('astroglobe', 'Indexed Documents') }}</div>
<div class="mcp-status-value">{{ vectorStatus.indexed_documents || 0 }}</div>
<div class="mcp-status-label">
{{ t('astroglobe', 'Indexed Documents') }}
</div>
<div class="mcp-status-value">
{{ vectorStatus.indexed_documents || 0 }}
</div>
</div>
<div class="mcp-status-card">
<div class="mcp-status-label">{{ t('astroglobe', 'Pending Documents') }}</div>
<div class="mcp-status-value">{{ vectorStatus.pending_documents || 0 }}</div>
<div class="mcp-status-label">
{{ t('astroglobe', 'Pending Documents') }}
</div>
<div class="mcp-status-value">
{{ vectorStatus.pending_documents || 0 }}
</div>
</div>
<div v-if="vectorStatus.last_sync_time" class="mcp-status-card">
<div class="mcp-status-label">{{ t('astroglobe', 'Last Sync') }}</div>
<div class="mcp-status-value">{{ vectorStatus.last_sync_time }}</div>
<div class="mcp-status-label">
{{ t('astroglobe', 'Last Sync') }}
</div>
<div class="mcp-status-value">
{{ vectorStatus.last_sync_time }}
</div>
</div>
</div>
<NcButton type="secondary" @click="loadVectorStatus" :disabled="statusLoading">
<NcButton type="secondary" :disabled="statusLoading" @click="loadVectorStatus">
<template #icon>
<Refresh :size="20" />
</template>
@@ -308,9 +322,15 @@
<!-- Text Viewer (for non-PDFs) -->
<div v-else class="mcp-text-viewer">
<div v-if="viewerContext.before" class="mcp-context-text">{{ viewerContext.before }}</div>
<div class="mcp-highlighted-chunk">{{ viewerContext.chunk }}</div>
<div v-if="viewerContext.after" class="mcp-context-text">{{ viewerContext.after }}</div>
<div v-if="viewerContext.before" class="mcp-context-text">
{{ viewerContext.before }}
</div>
<div class="mcp-highlighted-chunk">
{{ viewerContext.chunk }}
</div>
<div v-if="viewerContext.after" class="mcp-context-text">
{{ viewerContext.after }}
</div>
</div>
</div>
</div>
@@ -337,8 +357,6 @@ import Cog from 'vue-material-design-icons/Cog.vue'
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
import Refresh from 'vue-material-design-icons/Refresh.vue'
import TextBoxOutline from 'vue-material-design-icons/TextBoxOutline.vue'
import TextBoxRemoveOutline from 'vue-material-design-icons/TextBoxRemoveOutline.vue'
import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
import Eye from 'vue-material-design-icons/Eye.vue'
import Close from 'vue-material-design-icons/Close.vue'
@@ -354,16 +372,13 @@ import * as pdfjsLib from 'pdfjs-dist'
try {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
import.meta.url,
).toString()
} catch (e) {
console.warn('Failed to set PDF.js worker, will use fallback', e)
// PDF.js will use fake worker automatically
}
// App name for translations
const APP_NAME = 'astroglobe'
export default {
name: 'App',
components: {
@@ -385,8 +400,6 @@ export default {
ChevronDown,
ChevronUp,
Refresh,
TextBoxOutline,
TextBoxRemoveOutline,
OpenInNew,
Eye,
Close,
@@ -754,7 +767,7 @@ export default {
closeViewer() {
this.showViewer = false
}
},
},
}
</script>
@@ -1177,7 +1190,7 @@ a.mcp-result-title {
.mcp-checkbox-grid {
grid-template-columns: 1fr;
}
.mcp-modal {
width: 100%;
height: 100%;
+5 -6
View File
@@ -59,7 +59,7 @@ function initSearchSettingsForm() {
const response = await axios.post(
generateUrl('/apps/astroglobe/api/admin/search-settings'),
data,
{ headers: { 'Content-Type': 'application/json' } }
{ headers: { 'Content-Type': 'application/json' } },
)
if (response.data.success) {
@@ -91,7 +91,7 @@ async function initWebhookManagement() {
try {
// Load webhook presets from API
const response = await axios.get(
generateUrl('/apps/astroglobe/api/admin/webhooks/presets')
generateUrl('/apps/astroglobe/api/admin/webhooks/presets'),
)
if (!response.data.success) {
@@ -115,7 +115,7 @@ async function initWebhookManagement() {
* Render webhook preset cards.
*
* @param {HTMLElement} container Container element
* @param {Object} presets Preset configurations
* @param {object} presets Preset configurations
*/
function renderWebhookPresets(container, presets) {
const presetIds = Object.keys(presets)
@@ -147,7 +147,7 @@ function renderWebhookPresets(container, presets) {
* Create a webhook preset card.
*
* @param {string} presetId Preset ID
* @param {Object} preset Preset configuration
* @param {object} preset Preset configuration
* @return {HTMLElement} Card element
*/
function createPresetCard(presetId, preset) {
@@ -212,7 +212,6 @@ async function togglePreset(presetId, currentlyEnabled) {
}
// Reload presets to update UI
const container = document.getElementById('webhook-presets-container')
await initWebhookManagement()
// Show success notification
@@ -227,7 +226,7 @@ async function togglePreset(presetId, currentlyEnabled) {
// Show error notification
OC.Notification.showTemporary(
error.message || 'Failed to toggle webhook preset',
{ type: 'error' }
{ type: 'error' },
)
}
}
+3 -3
View File
@@ -8,8 +8,8 @@
<AlertCircle :size="48" />
<p>{{ error }}</p>
</div>
<div v-else class="pdf-canvas-container" ref="container">
<canvas ref="canvas"></canvas>
<div v-else ref="container" class="pdf-canvas-container">
<canvas ref="canvas" />
</div>
<div v-if="!loading && !error && totalPages > 0" class="pdf-controls">
<NcButton
@@ -178,7 +178,7 @@ export default {
// Render page to canvas
const renderContext = {
canvasContext: context,
viewport: viewport,
viewport,
}
await page.render(renderContext).promise
+1 -1
View File
@@ -1,3 +1,3 @@
module.exports = {
extends: 'stylelint-config-recommended-vue',
extends: '@nextcloud/stylelint-config',
}
+22 -12
View File
@@ -113,9 +113,9 @@ style('astroglobe', 'astroglobe-settings');
<?php if (isset($_['serverStatus']['uptime_seconds'])): ?>
<?php
$uptime = $_['serverStatus']['uptime_seconds'];
$hours = floor($uptime / 3600);
$minutes = floor(($uptime % 3600) / 60);
p(sprintf('%d hours, %d minutes', $hours, $minutes));
$hours = floor($uptime / 3600);
$minutes = floor(($uptime % 3600) / 60);
p(sprintf('%d hours, %d minutes', $hours, $minutes));
?>
<?php else: ?>
<?php p($l->t('Unknown')); ?>
@@ -150,8 +150,8 @@ style('astroglobe', 'astroglobe-settings');
<td>
<?php
$status = $_['vectorSyncStatus']['status'] ?? 'unknown';
$statusClass = $status === 'idle' ? 'success' : ($status === 'syncing' ? 'info' : 'neutral');
?>
$statusClass = $status === 'idle' ? 'success' : ($status === 'syncing' ? 'info' : 'neutral');
?>
<span class="badge badge-<?php p($statusClass); ?>">
<?php p(ucfirst($status)); ?>
</span>
@@ -177,8 +177,8 @@ style('astroglobe', 'astroglobe-settings');
<td><strong><?php p($l->t('Errors (24h)')); ?></strong></td>
<td>
<?php
$errors = $_['vectorSyncStatus']['errors_24h'] ?? 0;
if ($errors > 0): ?>
$errors = $_['vectorSyncStatus']['errors_24h'] ?? 0;
if ($errors > 0): ?>
<span class="error"><?php p($errors); ?></span>
<?php else: ?>
<?php p('0'); ?>
@@ -213,13 +213,19 @@ style('astroglobe', 'astroglobe-settings');
<div class="mcp-form-group">
<label for="search-algorithm"><?php p($l->t('Search Algorithm')); ?></label>
<select id="search-algorithm" name="algorithm" class="mcp-select">
<option value="hybrid" <?php if ($_['searchSettings']['algorithm'] === 'hybrid') echo 'selected'; ?>>
<option value="hybrid" <?php if ($_['searchSettings']['algorithm'] === 'hybrid') {
echo 'selected';
} ?>>
<?php p($l->t('Hybrid (Recommended)')); ?>
</option>
<option value="semantic" <?php if ($_['searchSettings']['algorithm'] === 'semantic') echo 'selected'; ?>>
<option value="semantic" <?php if ($_['searchSettings']['algorithm'] === 'semantic') {
echo 'selected';
} ?>>
<?php p($l->t('Semantic Only')); ?>
</option>
<option value="bm25" <?php if ($_['searchSettings']['algorithm'] === 'bm25') echo 'selected'; ?>>
<option value="bm25" <?php if ($_['searchSettings']['algorithm'] === 'bm25') {
echo 'selected';
} ?>>
<?php p($l->t('Keyword (BM25) Only')); ?>
</option>
</select>
@@ -231,10 +237,14 @@ style('astroglobe', 'astroglobe-settings');
<div class="mcp-form-group">
<label for="search-fusion"><?php p($l->t('Fusion Method')); ?></label>
<select id="search-fusion" name="fusion" class="mcp-select">
<option value="rrf" <?php if ($_['searchSettings']['fusion'] === 'rrf') echo 'selected'; ?>>
<option value="rrf" <?php if ($_['searchSettings']['fusion'] === 'rrf') {
echo 'selected';
} ?>>
<?php p($l->t('RRF - Reciprocal Rank Fusion (Recommended)')); ?>
</option>
<option value="dbsf" <?php if ($_['searchSettings']['fusion'] === 'dbsf') echo 'selected'; ?>>
<option value="dbsf" <?php if ($_['searchSettings']['fusion'] === 'dbsf') {
echo 'selected';
} ?>>
<?php p($l->t('DBSF - Distribution-Based Score Fusion')); ?>
</option>
</select>