Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 24 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ permissions:
contents: read

jobs:
userscript:
if: inputs.suite != 'packages'
name: Userscript build & tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/scripts/baka-monkey
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
package_json_file: src/scripts/baka-monkey/package.json
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: pnpm
cache-dependency-path: src/scripts/baka-monkey/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm test

backend-main-shards:
if: inputs.suite != 'platforms' && inputs.suite != 'packages'
name: Backend main tests (${{ matrix.group }})
Expand Down Expand Up @@ -274,16 +294,17 @@ jobs:
frontend:
if: ${{ always() && inputs.suite != 'packages' }}
name: Frontend lint & tests
needs: [frontend-quality, frontend-build]
needs: [frontend-quality, frontend-build, userscript]
runs-on: ubuntu-latest
steps:
- name: Confirm frontend checks passed
env:
QUALITY_RESULT: ${{ needs.frontend-quality.result }}
BUILD_RESULT: ${{ needs.frontend-build.result }}
USERSCRIPT_RESULT: ${{ needs.userscript.result }}
run: |
if [[ "$QUALITY_RESULT" != success || "$BUILD_RESULT" != success ]]; then
echo "::error::Frontend lint & unit tests: $QUALITY_RESULT; frontend build: $BUILD_RESULT"
if [[ "$QUALITY_RESULT" != success || "$BUILD_RESULT" != success || "$USERSCRIPT_RESULT" != success ]]; then
echo "::error::Frontend lint & unit tests: $QUALITY_RESULT; frontend build: $BUILD_RESULT; userscript: $USERSCRIPT_RESULT"
exit 1
fi

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Bakabase.Abstractions.Models.Domain;

public record TaskSummary
{
public int Completed { get; set; }
public int Failed { get; set; }
public int Total { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bakabase.Abstractions.Models.Domain;
using Bakabase.Infrastructures.Components.Gui;
using Bakabase.InsideWorld.Business;
using Bakabase.InsideWorld.Business.Components.Configurations.Models.Domain;
Expand Down Expand Up @@ -66,6 +67,14 @@ public async Task<ListResponse<DownloadTask>> GetAll()
return new ListResponse<DownloadTask>(await _service.GetAllDto());
}

[HttpGet("summary")]
[RemoteAccessible]
[SwaggerOperation(OperationId = "GetDownloadTaskSummary")]
public async Task<SingletonResponse<TaskSummary>> GetSummary([FromQuery] ThirdPartyId? thirdPartyId = null)
{
return new SingletonResponse<TaskSummary>(await _service.GetSummary(thirdPartyId));
}

[SwaggerOperation(OperationId = "GetDownloadTask")]
[HttpGet("{id}")]
public async Task<SingletonResponse<DownloadTask>> Get(int id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Bakabase.Abstractions.Models.Domain;
using Bakabase.InsideWorld.Business.Components.PostParser.Models.Domain.Constants;
using Bakabase.InsideWorld.Business.Components.PostParser.Services;
using Bakabase.Service.Components.RemoteAccess;
using Bootstrap.Models.ResponseModels;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace Bakabase.Service.Controllers;

[Route("~/post-parser/task/summary")]
public sealed class PostParserTaskSummaryController(IPostParserTaskService service) : Controller
{
[HttpGet]
[RemoteAccessible]
[SwaggerOperation(OperationId = "GetPostParserTaskSummary")]
public async Task<SingletonResponse<TaskSummary>> Get([FromQuery] PostParserSource? source = null)
{
return new SingletonResponse<TaskSummary>(await service.GetSummary(source));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Bakabase.Abstractions.Models.Domain;
using Bakabase.InsideWorld.Business;
using Bakabase.Infrastructures.Components.Gui;
using Bakabase.InsideWorld.Business.Components.Configurations.Models.Domain;
Expand Down Expand Up @@ -713,6 +714,22 @@ public async Task<DownloadTask[]> GetAllDto()
return await ToDto(tasks);
}

public async Task<TaskSummary> GetSummary(ThirdPartyId? thirdPartyId = null)
{
var tasks = await GetAll(thirdPartyId.HasValue ? t => t.ThirdPartyId == thirdPartyId.Value : null);
var summary = new TaskSummary {Total = tasks.Count};
foreach (var task in tasks)
{
// Project the same live status as the list, without loading downloaded-file sizes.
switch (task.ToDomainModel(DownloaderManager)!.Status)
{
case DownloadTaskStatus.Complete: summary.Completed++; break;
case DownloadTaskStatus.Failed: summary.Failed++; break;
}
}
return summary;
}

/// <summary>
/// Tell, for each of <paramref name="keys"/>, whether the item is in the download list
/// right now (and under which task ids) and when it was last downloaded. Keys the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Bakabase.Abstractions.Models.Domain;
using Bakabase.InsideWorld.Business.Components.PostParser.Models.Domain;
using Bakabase.InsideWorld.Business.Components.PostParser.Models.Domain.Constants;
using Bootstrap.Components.Tasks;
Expand All @@ -11,6 +12,7 @@ namespace Bakabase.InsideWorld.Business.Components.PostParser.Services;
public interface IPostParserTaskService
{
Task<List<PostParserTask>> GetAll();
Task<TaskSummary> GetSummary(PostParserSource? source = null);
Task AddRange(Dictionary<PostParserSource, List<string>> sourceLinksMap, List<PostParseTarget> targets);
Task AddInputs(Dictionary<PostParserSource, List<string>> sourceLinksMap, List<PostParseTarget> targets,
List<string> links, string? text, string? title);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Threading;
using System.Threading.Tasks;
using Bakabase.Abstractions.Components.Tasks;
using Bakabase.Abstractions.Models.Domain;
using Bakabase.Abstractions.Models.Domain.Constants;
using Bakabase.InsideWorld.Business.Components.Gui;
using Bakabase.InsideWorld.Business.Components.Configurations.Models.Domain;
using Bakabase.InsideWorld.Business.Components.PostParser.Extensions;
Expand Down Expand Up @@ -47,6 +49,15 @@ public async Task<List<PostParserTask>> GetAll()
return result;
}

public async Task<TaskSummary> GetSummary(PostParserSource? source = null)
{
var all = await GetAll();
var activeTasks = tasks.Tasks.Where(t => t.Task.Status is BTaskStatus.NotStarted or BTaskStatus.Running
or BTaskStatus.Paused or BTaskStatus.Pausing or BTaskStatus.Resuming or BTaskStatus.Cancelling)
.Select(t => t.Id).ToHashSet();
return PostParserTaskSummary.Create(all, source, activeTasks);
}

public Task AddRange(Dictionary<PostParserSource, List<string>> sourceLinksMap, List<PostParseTarget> targets) =>
AddInputs(sourceLinksMap, targets, [], null, null);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Bakabase.Abstractions.Models.Domain;
using Bakabase.InsideWorld.Business.Components.PostParser.Models.Domain;
using Bakabase.InsideWorld.Business.Components.PostParser.Models.Domain.Constants;
using Bakabase.Modules.Workflow.Abstractions.Models.Domain.Constants;

namespace Bakabase.InsideWorld.Business.Components.PostParser.Services;

internal static class PostParserTaskSummary
{
public static TaskSummary Create(IEnumerable<PostParserTask> tasks, PostParserSource? source = null,
IReadOnlySet<string>? activeTasks = null)
{
var summary = new TaskSummary();
foreach (var task in tasks.Where(t => !t.IsDeleted && (!source.HasValue || t.Source == source.Value)))
{
summary.Total++;
// Match the parser page: an active/restarted workflow takes precedence over saved results.
if (task.WorkflowRunId is { } runId && activeTasks?.Contains($"workflow.run.{runId}") == true ||
task.WorkflowStatus is WorkflowRunStatus.Pending or WorkflowRunStatus.Running or WorkflowRunStatus.Interrupted)
continue;

if (!string.IsNullOrEmpty(task.Error) ||
task.WorkflowStatus is WorkflowRunStatus.Failed or WorkflowRunStatus.Cancelled ||
task.Targets.Any(target => Result(task, target).Error))
{
summary.Failed++;
continue;
}

var downloadInfo = Result(task, PostParseTarget.DownloadInfo).Data;
if (task.ParsingState is "awaitingAi" or "awaitingPurchase" or "possiblyExpired" or "partial" or "snapshotSaved" ||
task.ContentSnapshot?.Locks.Any(l => !l.IsBought) == true ||
downloadInfo?["isComplete"] is JsonValue complete && complete.TryGetValue<bool>(out var isComplete) && !isComplete ||
task.WorkflowStatus == WorkflowRunStatus.Waiting)
continue;

if (task.WorkflowStatus == WorkflowRunStatus.Success ||
task.WorkflowRunId == null && (task.ParsingState == "complete" ||
task.Targets.Count > 0 && task.Targets.All(target => Result(task, target).Data != null)))
summary.Completed++;
}
return summary;
}

// The parser page accepts both legacy data/error wrappers and current direct result objects.
private static (JsonObject? Data, bool Error) Result(PostParserTask task, PostParseTarget target)
{
if (task.Results?.GetValueOrDefault(target) is not JsonObject result) return (null, false);
if (result.ContainsKey("data") || result.ContainsKey("error") || result.ContainsKey("parsedAt"))
return (result["data"] as JsonObject,
result["error"] is JsonValue error && error.TryGetValue<string>(out var message) && !string.IsNullOrEmpty(message));
return (result, false);
}
}
4 changes: 2 additions & 2 deletions src/scripts/baka-monkey/package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "baka-monkey",
"private": true,
"version": "2.1.1",
"version": "2.1.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"test": "vite build && node --experimental-strip-types --test tests/*.test.ts tests/*.test.mjs",
"test": "pnpm build && node --experimental-strip-types --test tests/*.test.ts tests/*.test.mjs",
"preview": "vite preview"
},
"devDependencies": {
Expand Down
7 changes: 7 additions & 0 deletions src/scripts/baka-monkey/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ContentTrackerBadge } from './actions/ContentTrackerBadge';
import { startHeartbeat, isConnected, getConnectionState, onConnectionChange } from './heartbeat';
import { isCoverOverlayEnabled, onSettingsChange } from './settings';
import { t } from './i18n';
import type { TaskSummaryTarget } from './taskSummary';

interface MarkerEntry {
id: string;
Expand Down Expand Up @@ -94,6 +95,11 @@ export function App({ siteConfigs }: { siteConfigs: SiteConfig[] }) {
const hostname = window.location.hostname;
return siteConfigs.find((c) => c.domains.some((d) => hostname.includes(d))) ?? null;
}, [siteConfigs]);
const taskSummaryTarget = useMemo<TaskSummaryTarget | undefined>(() => {
if (siteConfig?.downloadTask) return { kind: 'download', source: siteConfig.downloadTask.thirdPartyId };
if (siteConfig?.parseTask) return { kind: 'parse', source: siteConfig.parseTask.source };
return undefined;
}, [siteConfig]);

// Start heartbeat and track connection state
useEffect(() => {
Expand Down Expand Up @@ -407,6 +413,7 @@ export function App({ siteConfigs }: { siteConfigs: SiteConfig[] }) {
<SettingsPanel
siteKey={siteConfig?.key}
coverOverlay={siteConfig?.coverOverlay}
taskSummaryTarget={taskSummaryTarget}
connection={connection}
/>
{(connected || __DEV__) && siteConfig && markers.flatMap((m) => {
Expand Down
42 changes: 41 additions & 1 deletion src/scripts/baka-monkey/src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ import { IoSettingsSharp, IoWarning } from 'react-icons/io5';
import { getApiBaseUrl, setApiBaseUrl, getStoredValue, setStoredValue, httpRequest } from '../api';
import { pingNow, type ConnectionState } from '../heartbeat';
import { getOverlayRoot } from '../overlay';
import { isCoverOverlayEnabled, setCoverOverlayEnabled } from '../settings';
import {
isCoverOverlayEnabled,
setCoverOverlayEnabled,
isTaskSummaryVisible,
setTaskSummaryVisible,
onSettingsChange,
} from '../settings';
import type { CoverOverlayConfig } from '../types';
import type { TaskSummaryTarget } from '../taskSummary';
import {
AUTO_TIMEZONE,
getBrowserTimeZone,
Expand All @@ -17,6 +24,7 @@ import {
setTimeZonePreference,
} from '../timezone';
import { showToast } from './Toast';
import { TaskSummaryPanel } from './TaskSummaryPanel';
import { t, getLocale, setLocale, onLocaleChange, describeRequestError, type Locale } from '../i18n';

const localeOptions: { key: Locale; label: string }[] = [
Expand All @@ -37,16 +45,19 @@ const MARKERS_VISIBLE_KEY = 'markers_visible_v2';
export function SettingsPanel({
siteKey,
coverOverlay,
taskSummaryTarget,
connection,
}: {
siteKey?: string;
/** Present when the current site can turn its covers into one big action button. */
coverOverlay?: CoverOverlayConfig;
taskSummaryTarget?: TaskSummaryTarget;
connection: ConnectionState;
}) {
const { connected, error: connectionError } = connection;
const [markersVisible, setMarkersVisible] = useState(() => getStoredValue(MARKERS_VISIBLE_KEY, true));
const [coverOverlayOn, setCoverOverlayOn] = useState(() => (siteKey ? isCoverOverlayEnabled(siteKey) : false));
const [taskSummaryVisible, setTaskSummaryVisibleState] = useState(() => (siteKey ? isTaskSummaryVisible(siteKey) : true));
const [apiUrl, setApiUrl] = useState(() => getApiBaseUrl());
const [apiUrlError, setApiUrlError] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(false);
Expand All @@ -61,6 +72,12 @@ export function SettingsPanel({

useEffect(() => onLocaleChange(() => forceUpdate((n) => n + 1)), []);

useEffect(() => {
const sync = () => setTaskSummaryVisibleState(siteKey ? isTaskSummaryVisible(siteKey) : true);
sync();
return onSettingsChange(sync);
}, [siteKey]);

// Reflect the persisted markers-visible preference onto <body> on mount.
useEffect(() => {
document.body.classList.toggle(HIDE_MARKERS_CLASS, !markersVisible);
Expand Down Expand Up @@ -98,6 +115,10 @@ export function SettingsPanel({
setCoverOverlayEnabled(siteKey, next);
};

const updateTaskSummaryVisibility = (visible: boolean) => {
if (siteKey) setTaskSummaryVisible(siteKey, visible);
};

const handleSaveApiUrl = () => {
try {
setApiUrl(setApiBaseUrl(apiUrl));
Expand Down Expand Up @@ -141,6 +162,14 @@ export function SettingsPanel({

return (
<div style={{ position: 'fixed', bottom: 20, right: 20, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8, zIndex: 10000 }}>
{siteKey && taskSummaryTarget && taskSummaryVisible && (
<TaskSummaryPanel
siteKey={siteKey}
target={taskSummaryTarget}
connection={connection}
onClose={() => updateTaskSummaryVisibility(false)}
/>
)}
{!connected && (
<div style={{
display: 'flex',
Expand Down Expand Up @@ -195,6 +224,17 @@ export function SettingsPanel({
>
{markersVisible ? t('hideMarkers') : t('showMarkers')}
</Button>
{siteKey && taskSummaryTarget && (
<Button
size="sm"
color={taskSummaryVisible ? 'warning' : 'success'}
variant="flat"
onPress={() => updateTaskSummaryVisibility(!taskSummaryVisible)}
style={{ width: '100%' }}
>
{taskSummaryVisible ? t('taskSummary.hide') : t('taskSummary.show')}
</Button>
)}
{coverOverlay && siteKey && (
<div className="flex flex-col gap-1">
<Button
Expand Down
Loading
Loading