Initial snapshot before step 10 package split

This commit is contained in:
admin
2026-05-22 21:39:09 +02:00
commit e029e898e9
16 changed files with 3955 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.pyc
scan-cancel.flag
cache.json
cache.json.tmp
reports/
.claude/
+71
View File
@@ -0,0 +1,71 @@
# rc-jav (Python CLI)
Session memory for Codex. Read before making changes here.
## What this is
A read-only rclone library comparison + search CLI. Compares `cq:JAV` remote (rclone crypt) against itself (dupe detection) or against external WinCatalog CSV/XML exports. Powers the rclone-jav Brave extension via native messaging.
## Architecture
```
rc-jav.py
├── reads config.json (default_target etc.)
├── reads cache.json (per-remote file index, written by --scan)
├── shells out to: rclone lsf / rclone lsjson / rclone size --json
├── extract_id() per filename → normalized ID with optional #partN / variant suffix
├── two query modes: --quick (live rclone --include glob) and cached (uses cache.json)
└── output: rich tables (default) | --basic plain | --format json (for extension)
```
## Files
```
D:\DEV\Project\rclone-jav\
├── rc-jav.py single-file CLI
├── config.json default_source/target/catalog (user-editable via --save)
├── cache.json scanned remote file index (written by --scan)
├── wincatalog\ drop WinCatalog CSV/XML exports here (auto-loaded)
├── TODO.md deferred work
└── README.md
```
## Companion project
`D:\DEV\Extensions\Production\rclone-jav\` (PC 1) / `D:\DEV\Extensions\Staging\rclone-jav\` (PC 2) — Brave extension + native messaging host that shells out to `rc-jav.py` for searches.
## ID normalization
- `extract_id()` chops trailing single letters (e.g. `IBW-902z.mp4``IBW-902`). Decision is intentional — see extension's AGENTS.md "Decision log".
- JAV IDs are canonicalized with at least 3 digits (`ABC-27``ABC-027`); 4+ digit IDs keep their width (`ABCD-1294`). User expects real JAV IDs to be `ABC-027`, never `ABC-27` or `ABC-0027`.
- Part suffix detection: `_1`, `-pt1`, `(1)` → appended as `#partN` for distinctness.
- Compound prefixes (`FC2-PPV-123`) handled via secondary regex.
- Search matcher does prefix lookup so `IBW-902` finds both `IBW-902` and `IBW-902#part1` etc.
- Quick search must emit only canonical padded uppercase globs (`ABC-027*`, `ABCDE-1167*`). Do not add `--ignore-case`; user never uses lowercase filenames and it caused noticeable delay.
## Defaults from earlier sessions
- `cq:JAV` is the current remote root (after the rclone crypt config change moved it down a level)
- `default_target` in config.json = `["cq:JAV"]`
- `human_size()` formats to 2 decimals (e.g. `6.94 GiB`)
- After the 3-digit ID canonicalization change, run `python rc-jav.py --scan` to rebuild `cache.json` under the new padded keys.
- Duplicate KEEP ranking uses configurable VIP folders before source/size/format ranking. Default VIP folder is `ClearJAV`; video files there are treated as the trusted direct-rip copy.
- Duplicate KEEP ranking treats `.ts` as the lowest-priority video container when any non-`.ts` duplicate is available.
## Recent decisions / bug fixes
- `--format json` should keep stdout as clean JSON. Status/progress text belongs on stderr in JSON mode.
- Catalog rows are informational. CSV exports mark them as `CATALOG`; JSON exports put them under `catalog`, not `delete_candidates`.
- Cache loading validates the top-level shape and falls back to an empty cache when `remotes` is missing or malformed.
- The old `--recursive/-R` flag was removed because scans are always recursive (`rclone lsf -R` / quick `lsjson -R`).
## TODO
See `TODO.md` for deferred work.
## When making changes
- Adding CLI flags: also update host invocation in `D:\DEV\Extensions\Production\rclone-jav\host\rcjav-host.py` if the flag matters to the extension
- Changing `extract_id()` semantics: forces a `--scan` to rebuild cache under new keys, and may need a parallel change in extension's `normalizeId()`
- JSON output format changes: extension's popup.js / overlay rendering reads `structured` array — keep field names stable (`source`, `remote`, `path`, `full_path`, `size`, `size_human`, `mod_time`, `jav_id`)
- Config schema: update `--save` writer and any defaults
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+59
View File
@@ -0,0 +1,59 @@
# rc-jav (Python CLI)
Session memory for Claude. Read before making changes here.
## What this is
A read-only rclone library comparison + search CLI. Compares `cq:JAV` remote (rclone crypt) against itself (dupe detection) or against external WinCatalog CSV/XML exports. Powers the rclone-jav Brave extension via native messaging.
## Architecture
```
rc-jav.py
├── reads config.json (default_target etc.)
├── reads cache.json (per-remote file index, written by --scan)
├── shells out to: rclone lsf / rclone lsjson / rclone size --json
├── extract_id() per filename → normalized ID with optional #partN / variant suffix
├── two query modes: --quick (live rclone --include glob) and cached (uses cache.json)
└── output: rich tables (default) | --basic plain | --format json (for extension)
```
## Files
```
D:\DEV\Project\rclone-jav\
├── rc-jav.py single-file CLI
├── config.json default_source/target/catalog (user-editable via --save)
├── cache.json scanned remote file index (written by --scan)
├── wincatalog\ drop WinCatalog CSV/XML exports here (auto-loaded)
├── TODO.md deferred work
└── README.md
```
## Companion project
`D:\DEV\Extensions\Production\rclone-jav\` (PC 1) / `D:\DEV\Extensions\Staging\rclone-jav\` (PC 2) — Brave extension + native messaging host that shells out to `rc-jav.py` for searches.
## ID normalization
- `extract_id()` chops trailing single letters (e.g. `IBW-902z.mp4` → `IBW-902`). Decision is intentional — see extension's CLAUDE.md "Decision log".
- Part suffix detection: `_1`, `-pt1`, `(1)` → appended as `#partN` for distinctness.
- Compound prefixes (`FC2-PPV-123`) handled via secondary regex.
- Search matcher does prefix lookup so `IBW-902` finds both `IBW-902` and `IBW-902#part1` etc.
## Defaults from earlier sessions
- `cq:JAV` is the current remote root (after the rclone crypt config change moved it down a level)
- `default_target` in config.json = `["cq:JAV"]`
- `human_size()` formats to 2 decimals (e.g. `6.94 GiB`)
## TODO
See `TODO.md`. Current item: WinCatalog `\` → `/` path normalization in load_catalog_*.
## When making changes
- Adding CLI flags: also update host invocation in `D:\DEV\Extensions\Production\rclone-jav\host\rcjav-host.py` if the flag matters to the extension
- Changing `extract_id()` semantics: forces a `--scan` to rebuild cache under new keys, and may need a parallel change in extension's `normalizeId()`
- JSON output format changes: extension's popup.js / overlay rendering reads `structured` array — keep field names stable (`source`, `remote`, `path`, `full_path`, `size`, `size_human`, `mod_time`, `jav_id`)
- Config schema: update `--save` writer and any defaults
+233
View File
@@ -0,0 +1,233 @@
# rc-jav
Read-only duplicate scanner for JAV files across rclone remotes. Groups files by JAV ID (e.g. `SSIS-001`) and reports which copy to keep based on priority rules.
## Priority rules
1. Video files inside configured **VIP folders** win first. Default VIP folder: `ClearJAV`.
2. If no VIP-folder video exists, **Source always wins** regardless of resolution/size.
3. `.ts` files rank below other video containers, even when the transport-stream copy is larger.
4. If no Source copy exists in the group, **largest file size wins** among the remaining Targets.
5. Suggestions only — script never deletes. Manual cleanup.
## ID matching
Filename stem is matched against:
- Primary: `^([A-Za-z]+)-(\d+)``SSIS-001`, `MIDV-123`, `ABP-456`
- Compound: `^(\w+(?:-\w+)+)-(\d+)``FC2-PPV-4894535`, `HEYZO-HD-1234`
- Fallback: `^([A-Za-z0-9]+)-(\d+)``1pondo-123`, `carib-456`
IDs normalized to uppercase with leading zeros stripped from the number (so `ssis-001` == `SSIS-1` == `SSIS-001`). Anything after the ID (` - Actress [1080p]`) is ignored for matching.
### Part-suffix handling
Multi-part files (`_1`, `_2`, `-1`, `-2`, `_A`, `_B`, `.1of4`, ` (1)`, `-pt1`, `-part1`, `-cd1`, `-disc1`, trailing ` N`) are normalized as `{ID}#partN` so they do not collide as false duplicates. Searching the base ID still finds all parts. Lettered `_A` / `_B` suffixes become part 1 / part 2.
Add more suffix shapes with repeatable `--part-pattern` regexes. The first capture group is the part number or one part letter and the pattern runs against the filename stem:
```powershell
python rc-jav.py --scan --part-pattern '[-_ ]side[-_ ]?(A|B)$'
python rc-jav.py --part-pattern '_([CD])$' --save
```
Saved rules live in `config.json` as `part_patterns`. The extension Options page has the same custom part detector list for host-triggered searches, duplicate review, and cache rebuilds.
Files with no parseable ID are listed under "Skipped" at the end so you can spot misnamed files.
### Rule checks
Focused rule tests cover ID extraction, multipart grouping safety, and duplicate KEEP ranking:
```powershell
python -B -m unittest discover -s tests -v
```
## Usage
```
python rc-jav.py \
--source cq:personal-files/ClearJAV/ichika-matsumoto \
--target cq:personal-files/JAV/TMP \
```
Flags:
- `--source` / `-s REMOTE` — priority remote path. Repeat for multiple.
- `--target` / `-t REMOTE` — non-priority remote path. Repeat for multiple.
- `--format {console,txt,csv,json,all}` — default `console`. Non-console formats write to `--output-dir`.
- `--output-dir DIR` — default `./reports`.
- `--no-color` — disable ANSI colors but keep rich layout (tables, panels).
- `--basic` — plain text output, no rich tables/panels. Progress ticks every 25 files on stderr. Useful for piping or simple terminals.
- `--rclone-bin PATH` — path to rclone executable (default: `rclone` on PATH). Example: `--rclone-bin C:\Programs\rclone\rclone.exe`.
- `--clearjav` — shortcut: sets source = `DEFAULT_SOURCE`, target = `DEFAULT_TARGET`. Equivalent to `--source cq:personal-files/ClearJAV --target cq:personal-files/JAV/TMP`. Combine with `--source`/`--target` to override one side.
Examples:
```
# full library dupe scan, one flag
python rc-jav.py --clearjav
# same but only check one actress folder against TMP
python rc-jav.py --clearjav --source cq:personal-files/ClearJAV/ichika-matsumoto
```
## Search mode
Check whether a JAV ID already exists in your library before downloading:
```
python rc-jav.py --search SSIS-001
python rc-jav.py --search SSIS-001 --search FC2-PPV-4894535
# wildcards (quote to avoid shell glob expansion)
python rc-jav.py --search "IPZZ-*"
python rc-jav.py --search "FC2-PPV-*"
python rc-jav.py --search "SSIS-???" # exact 3-digit numeric
```
Wildcard syntax: `*` (any chars) and `?` (one char), case-insensitive. Matches against normalized IDs in the index, including `#partN` suffixes automatically.
Range syntax: `[N-M]` inclusive both ends. Works inside any prefix.
```
python rc-jav.py --search "IPZZ-[820-860]"
python rc-jav.py --search "FC2-PPV-[4894500-4894600]"
python rc-jav.py --search "MIDV-[001-010]" # zero-padding preserved
```
Quote in PowerShell/bash so `[...]` reaches Python literally. Reversed ranges (`860-820`) auto-swap.
With no `--source` / `--target` flags, only `DEFAULT_TARGET` (TMP) is scanned — the typical case for "do I already have this in my unsorted pile?". Pass `--source cq:personal-files/ClearJAV` to also check the priority library. Edit `DEFAULT_SOURCE` / `DEFAULT_TARGET` at the top of the script to change defaults. Remote scans are recursive.
Exit code: `0` if every query had at least one hit, `1` otherwise — useful for shell automation.
## Name search (`--name`)
Substring search against filenames (case-insensitive). Find all files by actress, studio, tag, anything that appears in the filename.
```
python rc-jav.py --name Ichika
python rc-jav.py --name "Ichika Matsumoto"
python rc-jav.py --name Ichika --name Yui # OR — files matching either
python rc-jav.py --name "Mat*" # glob wildcard
python rc-jav.py --search IPZZ-860 --name Ichika # both — separate result blocks
```
- Multiple `--name` tokens = OR. Use one combined `--name "foo bar"` for AND/exact-substring.
- Matches against the filename stem only (not folder names).
- Auto-routes to **cached** mode because substring globs can't be server-side filtered on most backends. Pass `-q` to force quick anyway (slower).
### Smart search mode (auto quick / cached)
The script auto-picks the right execution path per query and prints which one it chose:
| Query shape | Picked mode | Reason |
|---|---|---|
| Single exact ID (`IPZZ-860`) | quick | live rclone `--include`, ~12s even on huge trees |
| Wildcard (`IPZZ-*`, `SSIS-???`) | cached | reliable normalized matching |
| Range (`IPZZ-[820-860]`) | cached | avoids N rclone calls |
| Multiple `--search` flags | cached | warmup amortizes |
Override:
- `--quick` / `-q` — force live rclone lookup (skips cache).
- `--cache` — force cache (builds it if cold).
Quick mode never reads or writes the cache. Cache mode honors `--update` and `--no-cache` as before.
### Cache
Search mode caches each remote's file list in `./cache.json` next to the script. Subsequent searches are near-instant.
- First run: scans + writes cache.
- Later runs: reads cache (banner shows `CACHED 14m (154 files)`).
- `--update` / `-u`: force re-scan + overwrite cache for the requested remotes.
- `--no-cache`: bypass cache (no read, no write).
- Stale warning when cache is older than 24h — still used, marked `CACHED-STALE`.
- Ctrl+C during a scan: rclone is terminated, cache for in-flight remote is NOT written.
Delete `cache.json` to reset everything.
### Saving defaults (--save)
Persist `--source`, `--target`, `--catalog`, and/or `--part-pattern` to `config.json` so you don't have to type them every run.
```
# set default target
python rc-jav.py --target cq:personal-files/JAV/TMP --save
# set source + multiple targets at once
python rc-jav.py --source cq:personal-files/ClearJAV ^
--target cq:personal-files/JAV/TMP ^
--target cq:personal-files/JAV/SORTED ^
--save
# inspect
type config.json
```
Only the keys you explicitly pass are written — running `--save --target X` won't wipe a saved `default_source`. Delete `config.json` to reset to the hardcoded defaults at the top of `rc-jav.py`.
### Scan-only (--scan)
Refresh the cache without running a search or dupe report — useful for Task Scheduler / cron pre-warming.
```
# default: refresh DEFAULT_TARGET (TMP)
python rc-jav.py --scan
# refresh both source and target
python rc-jav.py --scan --source cq:personal-files/ClearJAV --target cq:personal-files/JAV/TMP
# nightly via Task Scheduler
schtasks /Create /SC DAILY /ST 03:00 /TN "rc-jav nightly scan" ^
/TR "python D:\DEV\Project\rclone-jav\rc-jav.py --scan --basic"
```
`--scan` always overwrites the cache for the remotes you list. Exit 0 = success, non-zero = rclone failure.
```
python rc-jav.py --search MIDV-999 ; if ($LASTEXITCODE -eq 0) { "have it" } else { "download" }
```
## WinCatalog integration
WinCatalog's native `.wcat` format is proprietary, so the script reads its exports instead.
1. In WinCatalog: **File → Export** → choose **CSV** or **XML**.
2. Save into the `wincatalog/` folder next to the script. All `*.csv` and `*.xml` files there are auto-loaded — drop in as many discs as you want.
3. Run as normal: `python rc-jav.py --search IPZZ-860`
4. Override or add extra paths with `--catalog PATH` (file or folder, repeatable).
5. To change the default folder, edit `DEFAULT_CATALOG` at the top of the script.
Re-export when your catalog changes; the script re-reads on every run (catalog data is **not** cached — it's already a local file).
**Role of catalog hits:**
- Search: shown as rows with source label `Catalog`. The disc/volume name is encoded into the path so you know which offline backup holds the file.
- Dupe mode: catalog entries appear in groups for awareness but are **never marked KEEP or DELETE?** — they're offline, can't be touched. A group is only flagged as a dupe when 2+ rclone copies exist.
**CSV column auto-detection** (case-insensitive, first match wins):
- Name: `Name`, `File Name`, `Filename`, `Title`
- Path: `Path`, `Full Path`, `Location`, `Folder`
- Size: `Size`, `File Size`, `Bytes`, `Size (bytes)`
- Disc: `Disc`, `Disc Name`, `Disc Label`, `Volume`, `Source`, `Catalog`, `Media`
XML: walks the tree, treats `<File>` / `<f>` nodes inside `<Disc>` / `<Catalog>` / `<Volume>` containers, with `<Folder>` nesting.
## Requirements
- Python 3.9+
- `pip install rich` (used for progress bars + themed output)
- `rclone` on `PATH` with the relevant remotes configured.
## UI
- Live per-file progress bar during scans (`rclone size --json` for total, then `rclone lsf --files-only -R --format pst` streamed).
- Banner panel showing run mode + per-remote cache status.
- Rich tables for search hits and duplicate groups.
- `--no-color` for plain output (CI, piping).
## Roadmap
- Phase 1 (current): report duplicates + search.
- Phase 2: `--apply` mode that runs `rclone delete` on `DELETE?` candidates behind a confirmation gate.
- Phase 3: resolution-aware tiebreakers, move-to-review folder, scheduled runs.
+9
View File
@@ -0,0 +1,9 @@
# TODO / Deferred work
## Deferred
(append below)
## Completed notes
- WinCatalog CSV/XML paths are normalized from `\` to `/` during catalog load.
+5
View File
@@ -0,0 +1,5 @@
{
"default_target": [
"cq:JAV"
]
}
+83
View File
@@ -0,0 +1,83 @@
# Shared JAV ID fixture corpus
JSON cases shared between the Python `rc-jav.py` CLI and the browser
extension at `D:\DEV\Extensions\Production\rclone-jav\`. Each side
reads the cases relevant to its own extraction surface.
## Files
| File | Domain | Consumer | Notes |
|-------------------------------|----------|----------------------------------------|-------|
| `filename-extraction.json` | filename | Python `extract_id(name)` | Has `#partN` expectations for multipart files |
| `query-extraction.json` | query | Extension `content.js` `normalizeId` | Looser context; extension never emits part suffix |
| `shared-normalization.json` | shared | BOTH | Contract: any mismatch here is a bug, not a fixture issue |
All files share the same shape:
```json
{
"version": 1,
"domain": "…",
"description": "…",
"case_schema": { },
"cases": [
{ "name": "…", "input": "…", "expected": "…" }
]
}
```
`expected: null` means "no ID should be detected".
## Running the Python side
```bash
python fixtures/run.py
```
The runner imports `rc-jav.py` in place, exercises `extract_id` against
`filename-extraction.json`, and `normalize_id` against
`shared-normalization.json`. Exit code is non-zero on any failure.
## Running the extension side
No automated runner today. `content.js` lives inside an IIFE that the
browser injects into pages, so importing it from Node would require
either an extraction refactor or a duplicated copy of the regex. Until
that lands, treat `query-extraction.json` and `shared-normalization.json`
as the canonical specification: if you touch `ID_RE_DASHED`,
`ID_RE_UNDASHED`, or `BUILTIN_ID_NORMALIZERS` in content.js, eyeball
this corpus and confirm the cases still describe expected behavior.
## Adding a case
1. Pick the file matching the surface you're testing.
2. Append a `{ "name", "input", "expected" }` entry. Keep `name`
descriptive — it's the only label shown when the runner fails.
3. If the case exercises a guarantee both sides must honor, add it to
`shared-normalization.json` as well.
4. Run `python fixtures/run.py` to confirm Python still passes.
## Known cross-side divergences (intentional)
These are NOT bugs — they reflect the different surfaces each side
extracts from. Recorded here so future contributors don't try to
"fix" them.
- **`FC2PPV1841460` compact form (no dashes).** The extension's
`BUILTIN_ID_NORMALIZERS` in `content.js` rewrites this to
`FC2-PPV-1841460` when seen in page titles. Python `extract_id`
does NOT — the compact form doesn't realistically appear in
filenames on disk. Hence the case lives in
`query-extraction.json` only, not in `filename-extraction.json` or
`shared-normalization.json`.
If a case belongs to one side's contract but not the other's, file it
under the specific domain (`filename-` or `query-`) — not under
`shared-`.
## Ownership
This directory lives in the Python repo only because the Python repo
is the more stable root. Conceptually it's joint property of both
codebases. Don't add anything Python-specific to the JSON files — keep
them tool-neutral.
+24
View File
@@ -0,0 +1,24 @@
{
"version": 1,
"domain": "filename",
"description": "Filename → canonical JAV ID (with optional #partN suffix). Consumed by Python rc-jav.extract_id.",
"case_schema": {
"name": "human label",
"input": "filename including extension",
"expected": "canonical ID (e.g. ABC-001 or ABC-001#part1) or null when no ID present"
},
"cases": [
{ "name": "plain dashed ID", "input": "ABC-027.mp4", "expected": "ABC-027" },
{ "name": "dashed ID with resolution tag", "input": "SCOP-297 [1080p].mp4", "expected": "SCOP-297" },
{ "name": "bracket-wrapped ID", "input": "[REAL-779].mp4", "expected": "REAL-779" },
{ "name": "bracket-wrapped ID with extra tag", "input": "[SCOP-297] [1080p].mp4", "expected": "SCOP-297" },
{ "name": "no-hyphen fallback", "input": "MVSD312.avi", "expected": "MVSD-312" },
{ "name": "trailing lowercase variant letter", "input": "IBW-902z.mp4", "expected": "IBW-902z" },
{ "name": "multipart _PART suffix", "input": "KV-118 - Aiba Reika_PART1.mp4", "expected": "KV-118#part1" },
{ "name": "multipart _A letter suffix", "input": "KV-118_A.mp4", "expected": "KV-118#part1" },
{ "name": "multipart trailing -N before bracket", "input": "OFJE-195-7 [480p].mp4", "expected": "OFJE-195#part7" },
{ "name": "FC2 PPV plain", "input": "FC2-1841460.mp4", "expected": "FC2-PPV-1841460" },
{ "name": "FC2 PPV explicit", "input": "FC2-PPV-1841460.mp4", "expected": "FC2-PPV-1841460" },
{ "name": "no ID present", "input": "random_video.mp4", "expected": null }
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"version": 1,
"domain": "query",
"description": "Page text / title -> canonical JAV ID. Consumed by the browser extension (content.js normalizeId). Difference from filename: looser context (sentences, mixed punctuation, site chrome). Includes forms (e.g. FC2PPV compact) that Python extract_id does NOT handle, by design — see fixtures/README.md.",
"case_schema": {
"name": "human label",
"input": "raw page text",
"expected": "canonical ID without part suffix (extension never emits #partN), or null when no ID found"
},
"cases": [
{ "name": "title with site chrome", "input": "SSIS-001 — JAV.tube", "expected": "SSIS-001" },
{ "name": "title with description", "input": "Watch SSIS-001 1080p HD Online", "expected": "SSIS-001" },
{ "name": "trailing letter variant", "input": "IBW-902z Full Movie", "expected": "IBW-902" },
{ "name": "no hyphen in title", "input": "MVSD312 stream", "expected": "MVSD-312" },
{ "name": "FC2 PPV compact", "input": "FC2PPV-1841460 — preview", "expected": "FC2-PPV-1841460" },
{ "name": "FC2 plain digits", "input": "FC2-1841460 thumbnail", "expected": "FC2-PPV-1841460" },
{ "name": "FC2-PPV explicit", "input": "FC2-PPV-1841460 Full", "expected": "FC2-PPV-1841460" },
{ "name": "leading zeros preserved", "input": "ABF-042 — sample", "expected": "ABF-042" },
{ "name": "long numeric tail (7 digits)", "input": "BLK-4748520 stream", "expected": "BLK-4748520" },
{ "name": "no ID present", "input": "JAV Database · home", "expected": null }
]
}
+70
View File
@@ -0,0 +1,70 @@
"""Run the shared JAV-ID fixture corpus against rc-jav.py.
Exits non-zero if any fixture case fails. No third-party dependencies.
Usage:
python fixtures/run.py
"""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = Path(__file__).resolve().parent
SPEC = importlib.util.spec_from_file_location("rcjav", ROOT / "rc-jav.py")
RCJAV = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = RCJAV
SPEC.loader.exec_module(RCJAV)
def _load(name: str) -> dict:
with (FIXTURES / name).open("r", encoding="utf-8") as f:
return json.load(f)
def _run(label: str, cases: list[dict], fn) -> tuple[int, int]:
passed = 0
failed = 0
for case in cases:
got = fn(case["input"])
if got == case["expected"]:
passed += 1
else:
failed += 1
print(f" FAIL [{label}] {case['name']!r}")
print(f" input = {case['input']!r}")
print(f" expected = {case['expected']!r}")
print(f" got = {got!r}")
return passed, failed
def main() -> int:
total_passed = 0
total_failed = 0
for filename, fn_name, fn in [
("filename-extraction.json", "extract_id", RCJAV.extract_id),
("shared-normalization.json", "normalize_id", RCJAV.normalize_id),
]:
doc = _load(filename)
cases = doc.get("cases", [])
print(f"\n{filename} -> rcjav.{fn_name} ({len(cases)} cases)")
p, f = _run(filename, cases, fn)
total_passed += p
total_failed += f
print(f" {p} passed | {f} failed")
print()
if total_failed:
print(f"FAILED: {total_failed} of {total_passed + total_failed} cases")
return 1
print(f"OK: all {total_passed} cases passed")
return 0
if __name__ == "__main__":
sys.exit(main())
+17
View File
@@ -0,0 +1,17 @@
{
"version": 1,
"domain": "shared",
"description": "Raw ID forms → canonical form. Both Python (normalize_id) and the extension (content.js normalizeId) MUST agree on these. Mismatch here is a contract bug.",
"case_schema": {
"name": "human label",
"input": "raw ID-bearing token (no path, no extension)",
"expected": "canonical ID"
},
"cases": [
{ "name": "lowercase prefix uppercased", "input": "abc-027", "expected": "ABC-027" },
{ "name": "FC2 plain -> FC2-PPV", "input": "FC2-1841460", "expected": "FC2-PPV-1841460" },
{ "name": "FC2-PPV explicit preserved", "input": "FC2-PPV-1841460", "expected": "FC2-PPV-1841460" },
{ "name": "leading zeros preserved", "input": "ABF-042", "expected": "ABF-042" },
{ "name": "5-digit numeric segment", "input": "SDDE-12345", "expected": "SDDE-12345" }
]
}
+695
View File
@@ -0,0 +1,695 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>rclone-jav consolidation — final converged plan</title>
<style>
:root {
color-scheme: dark;
--bg: #0c0e10;
--shell: #14171a;
--panel: #181b1e;
--surface: #1f2327;
--line: #292e33;
--line-2: #3a4148;
--text: #e1e6eb;
--muted: #8a949d;
--blue: #6ec5ff;
--green: #7de4a0;
--yellow: #ffd36c;
--red: #ff9097;
--purple: #c5a9ff;
--orange: #ffb072;
}
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--text); font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
main { padding:24px; max-width:1380px; margin:0 auto; }
h1 { margin:0 0 4px; font-size:24px; }
h2 { margin:24px 0 8px; font-size:17px; color:#f4f7fa; }
h3 { margin:0 0 6px; font-size:11px; text-transform:uppercase; color:#9ba6af; letter-spacing:0.04em; }
p { margin:0; }
.intro { color:var(--muted); max-width:960px; margin:6px 0 18px; font-size:13px; }
.meta-banner { display:flex; align-items:center; gap:10px; padding:10px 14px; background:#11181f; border:1px solid #1f2b35; border-radius:6px; margin-bottom:18px; font-size:12px; color:var(--muted); }
.meta-banner .dot { width:8px; height:8px; border-radius:50%; background:var(--green); box-shadow:0 0 0 3px rgba(125,228,160,0.15); }
.meta-banner b { color:#cfdde5; }
.status-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; margin-bottom:24px; }
.status-card { background:#13171b; border:1px solid #232a30; border-radius:6px; padding:12px; }
.status-card h3 { color:#dce5ed; text-transform:none; letter-spacing:0; font-size:13px; margin-bottom:8px; }
.status-card.done { border-left:3px solid var(--green); }
.status-card.todo { border-left:3px solid var(--yellow); }
.status-card.work { border-left:3px solid var(--blue); }
.status-card ul { margin:0; padding-left:16px; color:var(--muted); font-size:12px; }
.status-card ul li { margin:2px 0; }
.status-card ul li b { color:#dce5ed; font-weight:600; }
.legend { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:14px; }
.pill { border-radius:12px; padding:3px 9px; font-size:11px; border:1px solid var(--line-2); background:#22272b; color:var(--text); }
.pill.keep { color:var(--green); background:#143020; border-color:#245036; }
.pill.move { color:var(--blue); background:#132837; border-color:#284b66; }
.pill.del { color:var(--red); background:#321618; border-color:#5b2228; }
.pill.debug{ color:var(--purple); background:#241d35; border-color:#453363; }
.pill.warn { color:var(--yellow); background:#332b16; border-color:#645228; }
.pill.ctx { color:var(--orange); background:#3a2818; border-color:#7a4b25; }
.mock { border:1px solid #2c333a; border-radius:8px; background:var(--shell); overflow:hidden; }
.top { display:flex; align-items:center; justify-content:space-between; padding:11px 14px; border-bottom:1px solid var(--line); background:#0f1214; }
.brand { font-weight:700; color:#fff; font-size:14px; letter-spacing:0.01em; }
.toolbar { display:flex; gap:6px; align-items:center; }
button { border:1px solid var(--line-2); border-radius:4px; padding:5px 10px; background:#252a2e; color:var(--text); font:inherit; cursor:default; }
button.primary { background:#163923; color:#aaf3bf; border-color:#285b3a; }
button.live { background:#143247; color:#9fd9ff; border-color:#2e607f; }
button.danger { background:#3a191d; color:#ffb2b7; border-color:#722c33; }
button.ghost { background:transparent; color:#9aa4ac; border-color:#3a4148; }
.layout { display:grid; grid-template-columns: 200px minmax(0,1fr); min-height:480px; }
.side { background:#0d1013; border-right:1px solid var(--line); padding:14px 12px; }
.grp { margin-bottom:16px; }
.gtitle { color:#5e6970; text-transform:uppercase; letter-spacing:0.06em; font-size:10px; margin:0 0 6px; padding-left:6px; }
.nav { display:grid; gap:2px; }
.nav a { display:flex; align-items:center; justify-content:space-between; padding:6px 8px; border-radius:4px; color:#b3bdc4; text-decoration:none; gap:6px; }
.nav a.active { background:#27313a; color:#fff; box-shadow:inset 2px 0 var(--blue); }
.nav a:hover:not(.active) { background:#1a1f24; }
.nav .badge { font-size:10px; padding:1px 6px; border-radius:9px; background:#2d343a; color:#a7b2bb; }
.nav .badge.warn { background:#3a3017; color:#ffd784; }
.nav .badge.fresh{ background:#1d3826; color:#9be3b3; }
.content { padding:18px 20px; min-width:0; }
.heading { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; margin-bottom:14px; }
.heading .desc { color:var(--muted); font-size:12px; margin-top:3px; }
.row-actions { display:flex; gap:6px; }
.sub-tabs { display:flex; gap:0; border-bottom:1px solid var(--line); margin-bottom:14px; }
.sub-tabs span { padding:8px 14px; color:#8b95a0; font-size:12px; cursor:default; border-bottom:2px solid transparent; }
.sub-tabs span.active { color:#fff; border-bottom-color: var(--blue); }
.grid2 { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap:10px; }
.grid3 { display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:10px; }
.card { background:var(--panel); border:1px solid var(--line); border-radius:6px; padding:12px; min-width:0; }
.card .meta { color:var(--muted); font-size:11px; margin-top:4px; }
.card .metric { font-size:18px; color:#fff; font-weight:600; }
.dup-list { display:grid; gap:6px; margin-top:8px; }
.dup-row { display:grid; grid-template-columns: 1fr auto auto; align-items:center; gap:8px; padding:8px 10px; background:#141a1e; border:1px solid #232a30; border-radius:4px; }
.dup-row .path { font:12px Consolas, monospace; color:#cdd6dd; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dup-row .size { font-size:11px; color:#8a949d; }
.keep-pill { font-size:10px; padding:1px 7px; border-radius:9px; background:#143b22; color:#9be3b3; border:1px solid #245036; }
.review-pill { font-size:10px; padding:1px 7px; border-radius:9px; background:#332b16; color:#ffd784; border:1px solid #645228; }
/* Decision table */
table.spec { width:100%; border-collapse:collapse; margin-top:10px; font-size:12px; }
table.spec th { text-align:left; padding:8px 10px; background:#181d22; color:#cfdde5; font-weight:600; border-bottom:1px solid #2a3138; }
table.spec td { padding:9px 10px; border-bottom:1px solid #1c2126; color:var(--text); vertical-align:top; }
table.spec tr:nth-child(even) td { background:#10141a; }
table.spec td.order { font-family:Consolas,monospace; color:var(--blue); font-weight:600; }
table.spec td.surface { color:#dce5ed; }
table.spec td .pill { font-size:10px; }
table.spec td .small { color:var(--muted); font-size:11px; display:block; margin-top:2px; }
/* Popup launcher mock */
.popup-mock { background:#0e1114; border:1px dashed #394149; border-radius:6px; padding:18px; display:grid; place-items:center; }
.popup-frame { width:340px; border:1px solid var(--line-2); border-radius:6px; background:#181b1e; padding:12px; box-shadow:0 6px 18px rgba(0,0,0,.4); }
.popup-frame .ptop { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; }
.popup-frame input { width:100%; background:#0a0c0e; border:1px solid #313840; border-radius:3px; padding:6px 8px; color:var(--text); font:12px Consolas,monospace; }
.popup-frame .pchips { display:flex; gap:5px; flex-wrap:wrap; margin-top:8px; }
.annot { color:var(--muted); font-size:11px; margin-top:10px; font-style:italic; }
/* Checklist */
.checklist { background:#13171b; border:1px solid #232a30; border-radius:6px; padding:14px; margin-top:10px; }
.checklist h3 { color:#dce5ed; text-transform:none; letter-spacing:0; font-size:13px; margin-bottom:8px; }
.checkrow { display:grid; grid-template-columns: 22px 1fr auto; gap:8px; padding:7px 0; border-top:1px solid #1c2126; align-items:start; }
.checkrow:first-of-type { border-top:0; }
.checkrow .box { width:14px; height:14px; border:1px solid #4a5560; border-radius:3px; margin-top:3px; background:#0a0c0e; }
.checkrow .what { color:var(--text); font-size:12px; }
.checkrow .what .small { color:var(--muted); font-size:11px; }
.checkrow .who { color:#9aa4ac; font-size:11px; font-style:italic; }
/* Decisions / sub-cards */
.dec { background:#13171b; border:1px solid #232a30; border-radius:7px; padding:14px; }
.dec-head { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px; }
.dec-head h3 { color:#dce5ed; text-transform:none; font-size:13px; letter-spacing:0; margin:0; }
.dec-verdict { color:#a8b3bb; font-size:12px; margin-top:8px; }
.dec-verdict b { color:#ddebf3; }
.decisions-2 { display:grid; grid-template-columns: 1fr 1fr; gap:12px; margin-top:10px; }
/* Sequence */
.seq-list { display:grid; gap:6px; margin-top:10px; }
.seq-list .step { display:grid; grid-template-columns: 30px 1fr auto auto; gap:10px; padding:9px 12px; background:#13171b; border:1px solid #232a30; border-radius:5px; align-items:center; }
.seq-list .num { width:24px; height:24px; border-radius:50%; background:#18354a; color:var(--blue); display:grid; place-items:center; font-weight:700; font-size:11px; }
.seq-list .why { color:var(--muted); font-size:11px; }
.seq-list .risk { font-size:10px; padding:2px 7px; border-radius:8px; }
.seq-list .risk.low { color:#9be3b3; background:#0f2218; border:1px solid #1e3b27; }
.seq-list .risk.med { color:#ffd784; background:#231d10; border:1px solid #3d3018; }
.seq-list .risk.high { color:#ffb2b7; background:#2a1216; border:1px solid #4a1f25; }
.seq-list .deps { font-size:10px; color:#7a838c; font-family:Consolas,monospace; }
code { font-family:Consolas, monospace; background:#1a1f24; padding:1px 5px; border-radius:3px; color:#cfdde5; }
.strike { text-decoration:line-through; color:#6b757d; }
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.side { border-right:0; border-bottom:1px solid var(--line); }
.grid2, .grid3, .decisions-2, .status-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<main>
<h1>rclone-jav Consolidation — Final Converged Plan</h1>
<div class="meta-banner">
<span class="dot"></span>
<span><b>Status:</b> execution in progress. <b>Shipped:</b> steps 1 (Sim Dupe delete), 2 (CSS extraction), 3 (Transfer Assistant delete + Diagnostics replacement), 5 (Recent Activity + Search Troubleshooting → new Debug Tools pane). <b>Pending:</b> steps 6 (options.js split — Cache & Dup Review paired, biggest), 7a (Bulk Check standalone window), 8 (fixtures), 9 (cache contract), 10 (rc-jav.py split), 11 (host fast-path decision). See <code>D:\DEV\Extensions\Production\rclone-jav\AGENTS.md</code> "Console consolidation refactor — execution status" for current state.</span>
</div>
<div class="status-grid">
<div class="status-card done">
<h3>✓ Decided</h3>
<ul>
<li>Console / Settings / Support tri-split</li>
<li>Default landing = <b>Duplicate Review</b></li>
<li>Status badges on tabs, <b>no dashboard pane</b></li>
<li>Launcher pattern over toolbox</li>
<li>Keep Ranking nested in Dup Review</li>
<li>Sim Dupe → <b>delete</b>, samples/ HTML harness</li>
<li>Transfer wizard → <b>delete</b> after Diagnostics replacement verified</li>
<li>Bulk ID Check → <b>detached chrome.windows popup</b>, NOT a Console sidebar tab</li>
<li>Inline rule tests stay, standalone benches → Debug</li>
</ul>
</div>
<div class="status-card done">
<h3>✓ Shipped</h3>
<ul>
<li><b>Step 1:</b> Sim Dupe deleted from popup. <code>samples/sim-dupe.js</code> preserves payload.</li>
<li><b>Step 2:</b> CSS extracted → <code>options.css</code>. options.html 1179 → 794 lines.</li>
<li><b>Step 3:</b> Transfer wizard deleted. Diagnostics → Native host registration now shows Extension ID + Copy button.</li>
<li><b>Step 5:</b> Recent Activity + Search Troubleshooting moved to new Debug Tools pane. Scope verified by code read.</li>
</ul>
</div>
<div class="status-card work">
<h3>📋 Pending</h3>
<ul>
<li><b>Step 6:</b> options.js split (Cache + Dup Review paired). 3133-line file. Biggest, riskiest.</li>
<li><b>Step 7a:</b> bulk-check.html standalone + popup launcher.</li>
<li><b>Steps 810:</b> fixtures, cache contract, rc-jav.py split.</li>
<li><b>Step 11:</b> host fast-path benchmark + narrow/delete decision.</li>
</ul>
</div>
</div>
<!-- ====================================================== -->
<h2>1. Primary recommended layout</h2>
<p class="intro">Default landing = Duplicate Review (user's most-frequent maintenance workflow). Sidebar tab labels carry live status badges — no dashboard pane needed. Launcher pattern: heavy tools open focused panes, not nested fieldsets.</p>
<div class="mock">
<div class="top">
<span class="brand">rclone-jav</span>
<div class="toolbar">
<button>Profile: cq:JAV</button>
<button class="ghost"></button>
</div>
</div>
<div class="layout">
<aside class="side">
<div class="grp">
<div class="gtitle">Console</div>
<div class="nav">
<a href="#" class="active">Duplicate Review<span class="badge warn">27</span></a>
<a href="#">Cache &amp; Scans<span class="badge fresh">28m</span></a>
<a href="#">Library Issues<span class="badge">4</span></a>
</div>
<p style="color:#5e6970; font-size:10px; margin:6px 0 0 6px; font-style:italic;">Bulk Check lives in its own window — popup launcher, not sidebar.</p>
</div>
<div class="grp">
<div class="gtitle">Settings</div>
<div class="nav">
<a href="#">Profiles</a>
<a href="#">Scan Behavior</a>
<a href="#">Matching Rules</a>
<a href="#">Site Extraction</a>
<a href="#">Overlays</a>
<a href="#">Deletion</a>
</div>
</div>
<div class="grp">
<div class="gtitle">Support</div>
<div class="nav">
<a href="#">Diagnostics</a>
<a href="#">Debug Tools</a>
</div>
</div>
</aside>
<div class="content">
<div class="heading">
<div>
<h2 style="margin-top:0;">Duplicate Review <span class="pill warn" style="margin-left:6px;">27 pending</span></h2>
<p class="desc">After-upload workflow. Risky groups skipped by default. Keep Ranking lives here as configuration, not in a separate Settings tab.</p>
</div>
<div class="row-actions">
<button>Re-scan</button>
<button class="primary">Run Delete Queue (12)</button>
</div>
</div>
<div class="sub-tabs">
<span class="active">Pending Review</span>
<span>Skipped — Risky</span>
<span>Keep Ranking Rules</span>
<span>Delete History</span>
</div>
<div class="grid2">
<div class="card">
<h3>Filter (this tool only)</h3>
<div class="row-actions" style="margin-top:6px; flex-wrap:wrap;">
<button class="ghost">Multipart only</button>
<button class="ghost">VIP collision</button>
<button class="ghost">Size diff &gt; 100MB</button>
</div>
<p class="meta">Filters scoped — never exported as global settings.</p>
</div>
<div class="card">
<h3>Delete queue</h3>
<div class="metric">12 files · 47.3 GiB</div>
<p class="meta">Safety: VIP folders + multipart-risk paths auto-excluded.</p>
</div>
</div>
<div style="margin-top:12px;">
<h3>JBD-291 · 2 candidates</h3>
<div class="dup-list">
<div class="dup-row">
<span class="path">/JAV/clearjav/JBD-291 [1080p].mp4</span>
<span class="size">4.94 GiB</span>
<span class="keep-pill">KEEP</span>
</div>
<div class="dup-row">
<span class="path">/JAV/old/JBD-291.mp4</span>
<span class="size">3.82 GiB</span>
<button class="danger" style="padding:2px 8px; font-size:11px;">Queue delete</button>
</div>
</div>
</div>
<div style="margin-top:14px;">
<h3>OFJE-195 · multipart risk</h3>
<div class="dup-list">
<div class="dup-row">
<span class="path">/JAV/OFJE-195_PART1.mp4</span>
<span class="size">2.10 GiB</span>
<span class="review-pill">REVIEW</span>
</div>
<div class="dup-row">
<span class="path">/JAV/OFJE-195_PART2.mp4</span>
<span class="size">2.08 GiB</span>
<span class="review-pill">REVIEW</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ====================================================== -->
<h2>2. Decision table (refactor spec data)</h2>
<p class="intro">Each current pane mapped to its future home, treatment, ship order, and replacement work (if any). Ship order = execution sequence within phase 3 (UI consolidation). Steps share PR scope where useful.</p>
<div class="legend">
<span class="pill keep">KEEP visible</span>
<span class="pill ctx">CONTEXTUAL (lives with feature)</span>
<span class="pill move">MOVE (relocate)</span>
<span class="pill debug">DEBUG only</span>
<span class="pill del">DELETE</span>
</div>
<table class="spec">
<thead>
<tr>
<th style="width:42px;">#</th>
<th>Current surface</th>
<th>Future home</th>
<th>Treatment</th>
<th>Replacement</th>
</tr>
</thead>
<tbody>
<tr>
<td class="order">1</td>
<td class="surface">Sim Dupe popup action</td>
<td><code>samples/popup-states.html</code> (repo file)</td>
<td><span class="pill del">DELETE</span></td>
<td><span class="small">No product replacement. Repo HTML for layout testing only.</span></td>
</tr>
<tr>
<td class="order">2</td>
<td class="surface">CSS embedded in options.html</td>
<td>Per-pane <code>.css</code> files alongside per-pane JS</td>
<td><span class="pill move">EXTRACT</span></td>
<td><span class="small">No behavior change. Reduces options.html before JS split.</span></td>
</tr>
<tr>
<td class="order">3</td>
<td class="surface">Transfer Assistant wizard</td>
<td>(gone)</td>
<td><span class="pill del">DELETE</span></td>
<td><span class="small">Replacement = Diagnostics 3 actions (see §3 checklist). Delete <em>after</em> verification.</span></td>
</tr>
<tr>
<td class="order">4</td>
<td class="surface">Cache &amp; Scans pane</td>
<td>Console → Cache &amp; Scans</td>
<td><span class="pill keep">KEEP</span></td>
<td><span class="small">Paired with Dup Review extraction. State interface shared.</span></td>
</tr>
<tr>
<td class="order">4</td>
<td class="surface">Duplicate Review pane</td>
<td>Console → Duplicate Review <em>(default landing)</em></td>
<td><span class="pill keep">KEEP</span></td>
<td><span class="small">Same PR as Cache &amp; Scans. Reads cache state.</span></td>
</tr>
<tr>
<td class="order">4</td>
<td class="surface">Keep Ranking Rules pane</td>
<td>Duplicate Review → Keep Ranking Rules <em>(sub-tab)</em></td>
<td><span class="pill ctx">CONTEXTUAL</span></td>
<td><span class="small">Moves with Dup Review. Becomes nested sub-tab.</span></td>
</tr>
<tr>
<td class="order">4</td>
<td class="surface">VIP folders config</td>
<td>Duplicate Review → Keep Ranking Rules</td>
<td><span class="pill ctx">CONTEXTUAL</span></td>
<td><span class="small">Feature-specific config moves with feature.</span></td>
</tr>
<tr>
<td class="order">5</td>
<td class="surface">Recent Activity (search/page history)</td>
<td>Support → Debug Tools → Search Activity</td>
<td><span class="pill debug">DEBUG</span></td>
<td><span class="small">If audit deletion events also present, split out (pending verification).</span></td>
</tr>
<tr>
<td class="order">5</td>
<td class="surface">Search Troubleshooting</td>
<td>Support → Debug Tools</td>
<td><span class="pill debug">DEBUG</span></td>
<td><span class="small">Standalone bench. No edit locality.</span></td>
</tr>
<tr>
<td class="order">5</td>
<td class="surface">Page Extraction Test (standalone)</td>
<td>Support → Debug Tools</td>
<td><span class="pill debug">DEBUG</span></td>
<td><span class="small">Inline "Pick Element" variant stays in Site Extraction settings.</span></td>
</tr>
<tr>
<td class="order">5</td>
<td class="surface">Test ID Extraction (inline)</td>
<td>Settings → Matching Rules (collapsible per editor)</td>
<td><span class="pill ctx">CONTEXTUAL</span></td>
<td><span class="small">Editor feedback. Stays beside rule it tests.</span></td>
</tr>
<tr>
<td class="order">5</td>
<td class="surface">Test ID Extraction (standalone bench)</td>
<td>Support → Debug Tools</td>
<td><span class="pill debug">DEBUG</span></td>
<td><span class="small">Second row — split from inline version above.</span></td>
</tr>
<tr>
<td class="order">6</td>
<td class="surface">Library Issues pane</td>
<td>Console → Library Issues</td>
<td><span class="pill keep">KEEP</span></td>
<td><span class="small">Own tab + status badge. Rename UI nested as sub-tab.</span></td>
</tr>
<tr>
<td class="order">6</td>
<td class="surface">Bulk ID Check</td>
<td><code>bulk-check.html</code> — detached <code>chrome.windows</code> popup</td>
<td><span class="pill move">RESHAPE</span></td>
<td><span class="small">Removed from Console sidebar. Single entry path = popup launcher button → opens 640×540 detached window. Different tool type than Console panes (transient utility, no sidebar context).</span></td>
</tr>
<tr>
<td class="order">7</td>
<td class="surface">Profiles, Scan Behavior, Overlays, Deletion settings</td>
<td>Settings → (separate sub-tabs)</td>
<td><span class="pill keep">KEEP</span></td>
<td><span class="small">Settings sub-tabs split into separate JS files.</span></td>
</tr>
<tr>
<td class="order">7</td>
<td class="surface">Matching Rules / Site Extraction</td>
<td>Settings → (separate sub-tabs, inline tests retained)</td>
<td><span class="pill keep">KEEP</span></td>
<td><span class="small">Collapsible inline tester beside each rule.</span></td>
</tr>
<tr>
<td class="order">8</td>
<td class="surface">(new) Shared fixture corpus</td>
<td>Top-level <code>fixtures/</code> (neutral location)</td>
<td><span class="pill move">NEW</span></td>
<td><span class="small">Contract between extension and Python. Both consume.</span></td>
</tr>
<tr>
<td class="order">9</td>
<td class="surface">Cache contract design</td>
<td>CACHE_VERSION (exists) + ID_RULES_VERSION (new)</td>
<td><span class="pill move">NEW</span></td>
<td><span class="small">Schema bump = force rebuild. Rules bump = warn-and-mark-stale.</span></td>
</tr>
<tr>
<td class="order">10</td>
<td class="surface">rc-jav.py monolith</td>
<td><code>rcjav/</code> package (ids, cache, dupes, catalog, …)</td>
<td><span class="pill move">SPLIT</span></td>
<td><span class="small">After fixtures + tests + cache contract exist.</span></td>
</tr>
<tr>
<td class="order">11</td>
<td class="surface">Host fast-path search</td>
<td>Narrow / Delete / Keep — based on §4 benchmark</td>
<td><span class="pill warn">DECIDE</span></td>
<td><span class="small">Benchmark idle + under-scan latency first.</span></td>
</tr>
</tbody>
</table>
<!-- ====================================================== -->
<h2>3. Pre-execution checklists (user handoffs)</h2>
<div class="decisions-2">
<div class="checklist">
<h3>Diagnostics replacement verification (gates step 3 — Transfer wizard delete)</h3>
<div class="checkrow"><span class="box"></span><span class="what">Current extension ID shown as one-line text with copy-to-clipboard button<div class="small">Replaces wizard's "your extension ID is…" step.</div></span><span class="who">user opens<br>Diagnostics</span></div>
<div class="checkrow"><span class="box"></span><span class="what">Button labeled "Re-register host" that triggers register-host.bat path<div class="small">Replaces wizard's "run this script" step.</div></span><span class="who">user opens<br>Diagnostics</span></div>
<div class="checkrow"><span class="box"></span><span class="what">Verification result shown inline within 2s of register click<div class="small">Replaces wizard's "now check the result" step.</div></span><span class="who">user opens<br>Diagnostics</span></div>
<div class="checkrow"><span class="box"></span><span class="what">All three above visible without expanding collapsed sections (one screen)<div class="small">If buried in expandable cards, write better UI first.</div></span><span class="who">visual<br>inspection</span></div>
</div>
<div class="checklist">
<h3>Recent Activity scope test (settles split question)</h3>
<div class="checkrow"><span class="box"></span><span class="what">Open Recent Activity. Note current entry types visible.<div class="small">LIVE search, CACHE search, MATCH, NO_MATCH, NO_ID, page-check, etc.</div></span><span class="who">user</span></div>
<div class="checkrow"><span class="box"></span><span class="what">Perform a delete in Duplicate Review. Refresh Recent Activity.<div class="small">Single delete operation, any candidate.</div></span><span class="who">user</span></div>
<div class="checkrow"><span class="box"></span><span class="what">If delete event appears → audit value exists. Split into Dup Review → Delete History.<div class="small">If no → single role. Move entire log to Debug Tools.</div></span><span class="who">user reports</span></div>
</div>
</div>
<p class="annot">Cosmetic remaining: popup launcher button label "Bulk Check" vs icon-only. Either works. Default to label until popup row gets crowded.</p>
<!-- ====================================================== -->
<h2>4. Bulk Check — detached window pattern</h2>
<p class="intro">User clarified: Bulk Check is a transient utility, not a persistent Console surface. Doesn't fit sidebar-tab pattern alongside Dup Review / Cache &amp; Scans / Library Issues. Decision: standalone <code>bulk-check.html</code> opened as detached <code>chrome.windows</code> popup, no Console sidebar entry. Single canonical entry path = popup launcher button.</p>
<div class="grid2" style="margin-top:14px;">
<div>
<h3 style="text-transform:none; letter-spacing:0; font-size:13px; color:#dce5ed; margin-bottom:8px;">Browser-action popup with launcher</h3>
<div class="popup-mock">
<div class="popup-frame">
<div class="ptop">
<b style="color:#fff; font-size:13px;">rclone-jav</b>
<button class="ghost" style="padding:2px 8px; font-size:11px;"></button>
</div>
<input value="BLK-474" />
<div class="pchips">
<button class="primary" style="padding:3px 10px; font-size:11px;">Search</button>
<button class="live" style="padding:3px 10px; font-size:11px;">📋 Bulk Check…</button>
<button class="ghost" style="padding:3px 10px; font-size:11px;"></button>
</div>
<div class="annot" style="margin-top:10px;">Single-search stays primary. Bulk button = one click to detached window.</div>
</div>
</div>
</div>
<div>
<h3 style="text-transform:none; letter-spacing:0; font-size:13px; color:#dce5ed; margin-bottom:8px;">Detached window (640×540) after launch</h3>
<div class="popup-mock" style="padding:10px;">
<div style="width:480px; max-width:100%; background:#181b1e; border:1px solid var(--line-2); border-radius:6px; box-shadow:0 8px 24px rgba(0,0,0,.5); overflow:hidden;">
<div style="display:flex; justify-content:space-between; align-items:center; background:#0f1214; border-bottom:1px solid var(--line); padding:8px 12px;">
<b style="color:#fff; font-size:13px;">Bulk ID Check</b>
<div style="display:flex; gap:4px;">
<button class="ghost" style="padding:2px 7px; font-size:11px;">_</button>
<button class="ghost" style="padding:2px 7px; font-size:11px;"></button>
<button class="ghost" style="padding:2px 7px; font-size:11px;">×</button>
</div>
</div>
<div style="padding:12px;">
<textarea style="width:100%; height:90px; background:#0a0c0e; border:1px solid #313840; border-radius:3px; padding:7px 9px; color:var(--text); font:12px Consolas,monospace; resize:vertical;">BLK-474
FC2-PPV-1841460
KV-118
PRTD-029
JBD-291</textarea>
<div class="row-actions" style="margin-top:8px;">
<button class="primary" style="font-size:12px;">Check 5 IDs</button>
<button style="font-size:12px;">Clear</button>
<button class="ghost" style="font-size:12px;">Import .txt…</button>
</div>
<div style="margin-top:12px; padding:8px 10px; background:#0f1518; border:1px solid #1d2a30; border-radius:4px; font-size:11px; color:var(--muted);">
5 IDs checked · <span style="color:#9be3b3;">3 match</span> · <span style="color:#ffd784;">2 no match</span>
</div>
<div class="dup-list" style="margin-top:8px;">
<div class="dup-row" style="padding:6px 8px;"><span class="path">BLK-474</span><span class="size">4.94 GiB</span><span class="keep-pill">MATCH</span></div>
<div class="dup-row" style="padding:6px 8px;"><span class="path">KV-118</span><span class="size">multi-part</span><span class="keep-pill">MATCH</span></div>
<div class="dup-row" style="padding:6px 8px;"><span class="path">PRTD-029</span><span class="size"></span><span class="review-pill">NO MATCH</span></div>
</div>
</div>
</div>
</div>
<p class="annot">Detached window. No tab bar, no address bar. Closes cleanly when done. Sits over browser, stays visible across tab switches.</p>
</div>
</div>
<div class="card" style="background:#13171b; border:1px solid #232a30; margin-top:14px;">
<h3 style="color:#dce5ed; text-transform:none; letter-spacing:0; font-size:13px;">Why detached window, not Console tab</h3>
<div class="grid2" style="margin-top:8px;">
<div>
<p class="meta" style="font-size:11px;"><b style="color:#cfdde5;">Other Console tools (Dup Review, Cache &amp; Scans, Library Issues):</b></p>
<ul style="color:var(--muted); font-size:11px; margin:6px 0 0 16px; padding:0;">
<li>long workflows, multi-pass</li>
<li>need sidebar context (compare to other tools)</li>
<li>persistent state (review queue, scan job)</li>
<li>fit Options sidebar tab pattern</li>
</ul>
</div>
<div>
<p class="meta" style="font-size:11px;"><b style="color:#cfdde5;">Bulk Check:</b></p>
<ul style="color:var(--muted); font-size:11px; margin:6px 0 0 16px; padding:0;">
<li>short workflow, one-shot</li>
<li>no sidebar context needed</li>
<li>transient state (last-paste persisted, results ephemeral)</li>
<li>fits detached-window pattern</li>
</ul>
</div>
</div>
<p class="annot" style="margin-top:10px;">Different tool type. Treating it like Dup Review was a category error. Single user knows the feature exists — discovery via popup button is enough.</p>
</div>
<div class="card" style="background:#13171b; border:1px solid #232a30; margin-top:10px;">
<h3 style="color:#dce5ed; text-transform:none; letter-spacing:0; font-size:13px;">Implementation notes</h3>
<pre style="font-family:Consolas,monospace; font-size:11px; color:#cfdde5; background:#0a0c0e; padding:10px; border-radius:4px; margin:8px 0 0; overflow-x:auto; white-space:pre-wrap;">// Popup launcher click handler
chrome.windows.create({
url: chrome.runtime.getURL('bulk-check.html'),
type: 'popup',
width: 640,
height: 540
});</pre>
<ul style="color:var(--muted); font-size:12px; margin:8px 0 0 16px; padding:0;">
<li><b style="color:#dce5ed;">Window dedup:</b> track open bulk-check window ID in <code>chrome.storage.session</code>. Second launcher click focuses existing window instead of spawning duplicate.</li>
<li><b style="color:#dce5ed;">State persistence:</b> last paste saved to <code>chrome.storage.local</code> key <code>bulk_check_last_paste</code>. Reopen restores. Results are ephemeral (re-run on reopen).</li>
<li><b style="color:#dce5ed;">Backend reuse:</b> calls native host via same messaging path popup search uses. No new backend code.</li>
<li><b style="color:#dce5ed;">No back nav:</b> window can't navigate. User closes when done. Ctrl+W closes the bulk window, not a browser tab.</li>
</ul>
</div>
<div class="card" style="background:#13171b; border:1px solid #232a30; margin-top:10px;">
<h3 style="color:#dce5ed; text-transform:none; letter-spacing:0; font-size:13px;">Edge cases</h3>
<ul style="color:var(--muted); font-size:12px; margin:8px 0 0 16px; padding:0;">
<li><b style="color:#dce5ed;">Popup auto-closes after launcher click</b> (Chrome behavior). Window survives. Good — that's the intent.</li>
<li><b style="color:#dce5ed;">Window positioning unreliable.</b> Chrome treats <code>left/top</code> as hints, multi-monitor users may get the window on the wrong screen. Acceptable for personal-use tool.</li>
<li><b style="color:#dce5ed;">Brave / Edge variance.</b> Detached popups behave slightly differently across Chromium forks. Test on user's actual browser before shipping. Fallback if broken: open <code>bulk-check.html</code> in a normal tab via <code>chrome.tabs.create</code>.</li>
</ul>
</div>
<div class="card" style="background:#13171b; border:1px solid #232a30; margin-top:10px;">
<h3 style="color:#dce5ed; text-transform:none; letter-spacing:0; font-size:13px;">Does NOT generalize</h3>
<p class="meta" style="font-size:12px;">Detached-window pattern fits Bulk Check because it's transient + no-sidebar-context + short. Doesn't apply to:</p>
<ul style="color:var(--muted); font-size:12px; margin:8px 0 0 16px; padding:0;">
<li>Diagnostics — reference info, lives in sidebar fine</li>
<li>Setup repair button — already inline in Diagnostics, small enough</li>
<li>Dup Review / Cache &amp; Scans / Library Issues — long workflows, sidebar context useful</li>
<li>Settings — set-and-forget, not workflow</li>
</ul>
<p class="annot" style="margin-top:10px;">One-tool answer, not a pattern across the app.</p>
</div>
<!-- ====================================================== -->
<h2>5. Execution sequence (final)</h2>
<p class="intro">Codex's revised order (triage first, boundary doc second) with my refinements. Risk and dependencies marked. Steps 14 are reversible single-file changes (warmup phase). Steps 510 = structural. Step 11 = final architectural call.</p>
<div class="seq-list">
<div class="step"><span class="num">1</span><div><b>Per-pane triage</b> <span class="why">— 30 min with user. Decision table above IS this artifact.</span></div><span class="risk low">zero risk</span><span class="deps">no deps</span></div>
<div class="step"><span class="num">2</span><div><b>Boundary ownership doc</b> <span class="why">— extension extracts query ID, Python owns filename semantics, host adapts. 1 hour, no code.</span></div><span class="risk low">zero risk</span><span class="deps">after #1</span></div>
<div class="step"><span class="num">3</span><div><b>Host fast-path benchmark</b> <span class="why">— latency under idle Python AND under scanning Python. Result gates step 11.</span></div><span class="risk low">measure only</span><span class="deps">no deps</span></div>
<div class="step"><span class="num">4</span><div><b>Delete confirmed surfaces</b> <span class="why">— Sim Dupe popup button (no replacement), Transfer wizard (after Diagnostics verification passes).</span></div><span class="risk low">trivial</span><span class="deps">after §3 checklist</span></div>
<div class="step"><span class="num">5</span><div><b>CSS extraction from options.html</b> <span class="why">— per-pane CSS files. No behavior change. Bisect-friendly.</span></div><span class="risk low">low</span><span class="deps">after #4</span></div>
<div class="step"><span class="num">6</span><div><b>options.js split: Cache &amp; Dup Review paired</b> <span class="why">— Dup Review reads cache state. Single PR extracts both. Keep Ranking moves with Dup Review.</span></div><span class="risk med">moderate</span><span class="deps">after #5</span></div>
<div class="step"><span class="num">7</span><div><b>options.js split: Debug Tools + Library Issues + Settings sub-tabs</b> <span class="why">— remaining Options extractions. Inline test components reused across rule editors. Bulk Check is NOT here — it's a new standalone file (step 7a).</span></div><span class="risk med">moderate</span><span class="deps">after #6</span></div>
<div class="step"><span class="num">7a</span><div><b>Create <code>bulk-check.html</code> standalone + popup launcher button</b> <span class="why">— new HTML file, own JS module, no Options dependency. Popup gets one button calling <code>chrome.windows.create</code>. Window dedup + state persistence in <code>chrome.storage</code>.</span></div><span class="risk low">additive</span><span class="deps">parallel to #7</span></div>
<div class="step"><span class="num">8</span><div><b>Shared fixture corpus</b> <span class="why">— top-level <code>fixtures/</code> (neutral). Python and extension both consume.</span></div><span class="risk low">additive</span><span class="deps">no blocking</span></div>
<div class="step"><span class="num">9</span><div><b>Cache contract design</b> <span class="why">— CACHE_VERSION (exists) + ID_RULES_VERSION (new). Schema vs semantics, two concepts.</span></div><span class="risk med">design decision</span><span class="deps">before #10</span></div>
<div class="step"><span class="num">10</span><div><b>rc-jav.py module split</b> <span class="why">— ids.py, cache.py, dupes.py, catalog.py, rclone_io.py, cli.py. Tests pre-exist via #8.</span></div><span class="risk med">code churn</span><span class="deps">after #8, #9</span></div>
<div class="step"><span class="num">11</span><div><b>Host narrow / keep / delete</b> <span class="why">— based on #3 benchmark. If under-scan responsiveness depends on host = keep narrow. If not = delete.</span></div><span class="risk high">behavior change</span><span class="deps">after #3, #10</span></div>
</div>
<!-- ====================================================== -->
<h2>6. Acceptance criteria template</h2>
<p class="intro">Each step in the sequence needs three things in the final spec: acceptance criterion, rollback procedure, touched-files list. Without these, "ship order N" is a wish not a plan. Template below — fill per step in spec doc.</p>
<div class="card" style="background:#13171b; border:1px solid #232a30;">
<h3>Template per sequence step</h3>
<div style="font-family:Consolas,monospace; font-size:12px; color:#cfdde5; background:#0a0c0e; padding:12px; border-radius:4px; margin-top:8px; white-space:pre-wrap;">step: 6
title: options.js split — Cache &amp; Dup Review paired
touched_files:
- options.html (script tag order changes)
- options.js (DELETE: cache section, dup review section, keep ranking section)
- options-cache.js (NEW)
- options-review.js (NEW)
- options-core.js (NEW: shared helpers, pane nav, save/load)
acceptance:
- Fresh extension reload, options.html opens
- Default landing = Duplicate Review tab
- Cache &amp; Scans tab loads, shows last scan timestamp
- Run Duplicate Review on existing cache — same result set as pre-refactor
- Keep Ranking Rules sub-tab inside Dup Review opens
- No console errors on load or interaction
rollback:
- git revert &lt;sha&gt;
- No data migration. Cache schema unchanged. Storage keys unchanged.
- Diagnostics-verified replacement of Transfer wizard remains intact (step 3 already shipped).</div>
</div>
<!-- ====================================================== -->
<h2>7. Out of scope (explicitly rejected)</h2>
<ul style="color:var(--muted); margin:0 0 30px 18px; font-size:13px;">
<li><b style="color:#dce5ed;">Dashboard pane</b> — tab badges replace. Adding a dashboard creates a feature sink.</li>
<li><b style="color:#dce5ed;">After-Upload workflow wizard page</b> — sidebar nav order already encodes the workflow.</li>
<li><b style="color:#dce5ed;">Matching Lab consolidation page</b> — inline tests cover editor needs, standalone bench in Debug covers diagnostic needs.</li>
<li><b style="color:#dce5ed;">Mode switcher top bar (Console / Settings / Support segmented control)</b> — sidebar groups do this.</li>
<li><b style="color:#dce5ed;">In-extension Sim Dupe / Debug Preview page</b> — repo HTML file is enough for single-user layout work.</li>
<li><b style="color:#dce5ed;">Popup bulk mode toggle</b> — popup stays single-job. Launcher button opens detached window, no inline bulk mode.</li>
<li><b style="color:#dce5ed;">Bulk ID Check as Console sidebar tab</b> — wrong tool type for sidebar pattern. Detached window matches its transient nature.</li>
<li><b style="color:#dce5ed;">Bulk Check as Options-page deep-link tab</b> — previously considered. Rejected: leaves a leftover tab open after use, Options sidebar adds noise to a one-shot tool.</li>
<li><b style="color:#dce5ed;">Frontend framework (React/Vue/Svelte)</b> — vanilla + ordered script files is correct for MV3 + project scale.</li>
<li><b style="color:#dce5ed;">Console.log telemetry for usage audit</b> — manual triage of single-user project beats instrumented signals.</li>
</ul>
<h2>8. Net position</h2>
<p class="intro" style="margin-bottom:30px;">Architecture decided. Three small user handoffs remain (Diagnostics verification, Recent Activity scope check, popup button label). After those, decision table expands into per-step spec with acceptance + rollback. Code work begins on step 1 (smallest, fastest, lowest risk). Total estimated execution span: phased over multiple PRs, no big-bang refactor.</p>
</main>
</body>
</html>
+355
View File
@@ -0,0 +1,355 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>rclone-jav console consolidation direction</title>
<style>
:root {
color-scheme: dark;
--bg: #0e1011;
--shell: #17191b;
--panel: #121416;
--surface: #1d2023;
--line: #2b3035;
--line-2: #3b434b;
--text: #dce2e7;
--muted: #87939d;
--blue: #71c5ff;
--green: #7de7a1;
--yellow: #ffd36e;
--red: #ff8e94;
--purple: #c5a9ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
letter-spacing: 0;
}
main { padding: 24px; max-width: 1660px; margin: 0 auto; }
h1 { margin: 0; font-size: 23px; }
h2 { margin: 0 0 10px; font-size: 15px; color: #f4f7fa; }
h3 { margin: 0 0 5px; font-size: 12px; text-transform: uppercase; color: #9da9b2; letter-spacing: 0; }
p { margin: 0; }
.intro { color: var(--muted); max-width: 950px; margin: 5px 0 18px; }
.legend { display:flex; gap:7px; flex-wrap:wrap; margin-bottom:18px; }
.pill, .badge, button {
border: 1px solid var(--line-2);
border-radius: 4px;
padding: 4px 9px;
background: #252a2e;
color: var(--text);
font: inherit;
}
.pill { border-radius: 12px; font-size: 11px; }
.pill.console { color:var(--green); background:#153020; border-color:#245337; }
.pill.settings { color:var(--blue); background:#142838; border-color:#284b66; }
.pill.support { color:var(--purple); background:#241d35; border-color:#453363; }
.pill.choice { color:var(--yellow); background:#332b16; border-color:#645228; }
.grid { display:grid; grid-template-columns: 1fr; gap:18px; align-items:start; }
.option {
border: 1px solid var(--line);
background: #141719;
border-radius: 8px;
padding: 13px;
min-width: 0;
}
.option-head { display:flex; justify-content:space-between; gap:10px; margin-bottom:10px; align-items:flex-start; }
.option-note { color:var(--muted); font-size:12px; }
.mock {
border:1px solid #333a41;
border-radius:8px;
background: var(--shell);
overflow:hidden;
min-height: 420px;
width: 100%;
}
.top {
display:flex; align-items:center; justify-content:space-between; gap:12px;
padding:11px 13px; border-bottom:1px solid var(--line); background:#111315;
}
.brand { font-weight:700; color:#fff; font-size:14px; }
.tabs, .toolbar, .chips { display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
button { padding:5px 10px; border-radius:4px; cursor:default; }
button.primary { background:#173923; color:#aaf3bf; border-color:#285b3a; }
button.live { background:#143247; color:#98d6ff; border-color:#2e607f; }
button.warn { background:#3a3217; color:#ffe28b; border-color:#66552a; }
button.danger { background:#3a191d; color:#ffb2b7; border-color:#722c33; }
.layout { display:grid; grid-template-columns: 150px minmax(0,1fr); min-height:368px; }
.side { background:#101214; border-right:1px solid var(--line); padding:11px; }
.g { margin-bottom:13px; }
.gtitle { color:#68747d; text-transform:uppercase; letter-spacing:0; font-size:10px; margin:0 0 5px; }
.nav { display:grid; gap:3px; }
.nav span { display:block; padding:6px 7px; border-radius:4px; color:#aeb8bf; }
.nav span.active { background:#27313a; color:#fff; box-shadow:inset 2px 0 var(--blue); }
.content { padding:12px; min-width:0; }
.heading { display:flex; justify-content:space-between; gap:10px; align-items:start; margin-bottom:10px; }
.desc { color:var(--muted); font-size:11px; }
.cards { display:grid; gap:9px; }
.two { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap:9px; }
.three { display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:8px; }
.card, .panel, .modal, .popup, .step {
border:1px solid var(--line);
background:var(--panel);
border-radius:6px;
padding:10px;
min-width:0;
}
.card strong { color:#fff; }
.meta { color:var(--muted); font-size:11px; }
.mono { font-family:Consolas, monospace; font-size:11px; }
.metric { font-size:18px; color:#fff; font-weight:700; }
.line { border-top:1px solid #23282c; margin:8px 0; }
.status { border-radius:5px; padding:7px 8px; border:1px solid #294b34; background:#14301e; color:#b6f5c7; }
.status.warn { border-color:#5d5225; background:#332d16; color:#ffe188; }
.list { display:grid; gap:5px; margin-top:7px; }
.row { display:flex; justify-content:space-between; gap:8px; align-items:center; border-top:1px solid #23282c; padding-top:5px; min-width:0; }
.row:first-child { border-top:0; padding-top:0; }
.path { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:#b9c6ce; }
.bar { height:5px; background:#272c31; border-radius:99px; overflow:hidden; margin-top:7px; }
.fill { height:100%; width:72%; background:linear-gradient(90deg,var(--blue),var(--green)); }
.launchers { display:grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap:8px; }
.launcher { border:1px dashed #44515b; border-radius:6px; padding:11px; background:#12191f; }
.launcher b { display:block; color:#fff; margin-bottom:4px; }
.modal-stage { position:relative; min-height:250px; background:#0d1012; border:1px dashed #394149; border-radius:7px; padding:14px; }
.modal { width:min(100%, 385px); margin:22px auto 0; box-shadow:0 18px 46px rgba(0,0,0,.45); }
.workflow { display:grid; gap:8px; }
.step { display:grid; grid-template-columns:28px minmax(0,1fr) auto; align-items:center; gap:8px; }
.num { width:24px; height:24px; border-radius:50%; display:grid; place-items:center; background:#18354a; color:var(--blue); font-weight:700; }
.popup-stage { display:grid; place-items:center; min-height:340px; background:#0d1012; border:1px dashed #394149; border-radius:7px; padding:14px; }
.popup { width:360px; box-shadow:0 14px 42px rgba(0,0,0,.45); }
input, textarea {
width:100%; background:#0c0e10; color:var(--text); border:1px solid #323940; border-radius:4px;
padding:7px 8px; font:12px Consolas, monospace;
}
textarea { min-height:74px; resize:none; }
.seg { display:flex; border:1px solid var(--line-2); border-radius:4px; overflow:hidden; }
.seg span { min-width:58px; text-align:center; padding:4px 7px; color:#8b97a0; background:#22272b; border-right:1px solid var(--line-2); font-size:11px; }
.seg span:last-child { border-right:0; }
.seg span.active { color:var(--blue); background:#173043; }
.debug-grid { display:grid; grid-template-columns: 180px minmax(0,1fr); gap:9px; }
.preview { border:1px solid #2d3640; background:#101820; border-radius:6px; padding:9px; }
.preview-hit { border:1px solid #2d5339; background:#14241a; border-radius:5px; padding:7px; margin-top:7px; }
.bench-switch { display:flex; gap:5px; flex-wrap:wrap; margin-bottom:8px; }
.bench-switch span { border:1px solid var(--line-2); border-radius:12px; padding:3px 8px; font-size:11px; color:#adb7bf; }
.bench-switch span.active { color:var(--yellow); background:#332b16; border-color:#645228; }
.verdict { margin-top:9px; color:#aeb8bf; font-size:12px; }
@media (max-width: 760px) {
main { padding:14px; }
.layout, .debug-grid { grid-template-columns:1fr; }
.side { border-right:0; border-bottom:1px solid var(--line); }
.two, .three, .launchers { grid-template-columns:1fr; }
.popup { width:100%; }
}
</style>
</head>
<body>
<main>
<h1>rclone-jav Consolidation Direction</h1>
<p class="intro">Updated after the refactor discussion. The page keeps the earlier visual samples, but the decisions are now explicit: launcher-style maintenance console, Duplicate Review as the default work surface, status on navigation instead of a dashboard pane, Bulk ID Check with a popup-launched quick window plus a full Console tool, and debug/testing surfaces pulled out of the daily workflow.</p>
<div class="legend">
<span class="pill console">Frequent maintenance</span>
<span class="pill settings">Set-and-forget settings</span>
<span class="pill support">Support / debug</span>
<span class="pill choice">Placement decision</span>
</div>
<div class="grid">
<section class="option">
<div class="option-head"><div><h2>1. Recommended Console Shell</h2><p class="option-note">Frequent maintenance tools get focused destinations. The navigation carries status instead of a separate dashboard pane.</p></div><span class="pill console">chosen direction</span></div>
<div class="mock">
<div class="top"><span class="brand">rclone-jav Console</span><div class="toolbar"><button>Profile: cq:JAV</button><button>Options</button></div></div>
<div class="layout">
<aside class="side">
<div class="g"><div class="gtitle">Console</div><div class="nav"><span class="active">Duplicate Review [27]</span><span>Cache & Scans [28m]</span><span>Library Issues [4]</span><span>Bulk ID Check</span></div></div>
<div class="g"><div class="gtitle">Settings</div><div class="nav"><span>Profiles</span><span>Scan Behavior</span><span>Matching Rules</span><span>Deletion</span></div></div>
<div class="g"><div class="gtitle">Support</div><div class="nav"><span>Diagnostics</span><span>Debug Tools</span></div></div>
</aside>
<div class="content">
<div class="heading"><div><h2>Duplicate Review</h2><p class="desc">Default landing after uploads. Keep Ranking Rules and delete history stay with the workflow that uses them.</p></div><span class="pill choice">27 pending</span></div>
<div class="two">
<div class="card"><h3>Review Queue</h3><div class="metric">12 ready · 2 risky</div><p class="meta">Uses VIP folders, multipart safety, keep reasons, and delete queue checks.</p><div class="chips"><button class="primary">Review Duplicates</button><button>Skipped Risks</button></div></div>
<div class="card"><h3>Contextual Config</h3><p>Keep Ranking Rules live inside Duplicate Review, not as a distant general setting.</p><div class="chips"><button>Keep Ranking Rules</button><button>Delete History</button></div></div>
</div>
<div class="two" style="margin-top:9px;">
<div class="card"><h3>Cache Status Lives In Nav</h3><p>Cache & Scans owns the scan detail. The sidebar badge is enough while you review dupes.</p><button>Open Cache & Scans</button></div>
<div class="card"><h3>Console Neighbors</h3><p>Library Issues and Bulk ID Check remain direct tools, not settings fieldsets.</p><div class="chips"><button>Library Issues</button><button>Bulk ID Check</button></div></div>
</div>
</div>
</div>
</div>
<p class="verdict">Chosen: Console / Settings / Support sidebar, Duplicate Review first, no dashboard pane.</p>
</section>
<section class="option">
<div class="option-head"><div><h2>2. Launcher Treatment</h2><p class="option-note">The page should open focused maintenance views instead of collecting every heavy tool as a permanent fieldset.</p></div><span class="pill console">chosen pattern</span></div>
<div class="mock">
<div class="top"><span class="brand">Library Console</span><div class="tabs"><button class="live">Console</button><button>Settings</button><button>Support</button></div></div>
<div class="content">
<div class="two">
<div class="card"><h3>Cache Status</h3><div class="status">Fresh cache · cq:JAV · last scan 28m ago</div><div class="chips" style="margin-top:8px;"><button class="primary">Update Cache</button><button>Open Cache Console</button></div></div>
<div class="card"><h3>Next Maintenance</h3><p class="meta">Large tools stay out of the page until you open them.</p><div class="chips" style="margin-top:8px;"><button class="primary">Review Duplicates</button><button>Bulk Check</button><button>Library Issues</button></div></div>
</div>
<div class="modal-stage" style="margin-top:10px;">
<div class="meta">Example focused tool opened from a launcher</div>
<div class="modal">
<div class="heading"><div><h2>Duplicate Review</h2><p class="desc">Full space for filters, keep reasons, and delete queue safety.</p></div><button>x</button></div>
<div class="status warn">2 risky groups skipped by default</div>
<div class="list">
<div class="row"><span class="path">JBD-291 ClearJAV vs older MP4</span><span class="badge">KEEP</span></div>
<div class="row"><span class="path">OFJE-195 multipart warning</span><span class="badge">REVIEW</span></div>
</div>
</div>
</div>
</div>
</div>
<p class="verdict">Chosen for the big surfaces: Duplicate Review, Cache & Scans, Library Issues, and Bulk ID Check.</p>
</section>
<section class="option">
<div class="option-head"><div><h2>3. Boundary Without A Mode Switcher</h2><p class="option-note">Console, Settings, and Support stay distinct through sidebar groups, not a second top-level mode control.</p></div><span class="pill settings">chosen boundary</span></div>
<div class="mock">
<div class="top"><span class="brand">rclone-jav</span><div class="toolbar"><button class="live">Console</button><button>Settings</button><button>Support</button></div></div>
<div class="layout">
<aside class="side">
<div class="g"><div class="gtitle">Console</div><div class="nav"><span class="active">After Upload</span><span>Cache & Scans</span><span>Duplicate Review</span><span>Bulk ID Check</span></div></div>
</aside>
<div class="content">
<div class="three">
<div class="card"><span class="pill console">Console</span><h2 style="margin-top:7px;">Maintenance</h2><p class="meta">Review the library repeatedly.</p></div>
<div class="card"><span class="pill settings">Settings</span><h2 style="margin-top:7px;">Configure</h2><p class="meta">Profiles, rules, overlays, deletion.</p></div>
<div class="card"><span class="pill support">Support</span><h2 style="margin-top:7px;">Troubleshoot</h2><p class="meta">Diagnostics, debug benches, setup.</p></div>
</div>
<div class="panel" style="margin-top:9px;">
<h3>Settings mode would look quieter</h3>
<div class="chips"><button>Profiles</button><button>Scan Behavior</button><button>Overlays</button><button>Site Extraction</button><button>ID Rules</button><button>Deletion</button></div>
</div>
</div>
</div>
</div>
<p class="verdict">Chosen conceptually, simplified visually: sidebar groups do the separating work.</p>
</section>
<section class="option">
<div class="option-head"><div><h2>4. No Workflow Wizard</h2><p class="option-note">The maintenance order is real, but it should be encoded by the Console tools themselves rather than another page.</p></div><span class="pill choice">rejected surface</span></div>
<div class="mock">
<div class="top"><span class="brand">Console Order</span><div class="toolbar"><button>Profile: cq:JAV</button><button>Settings</button></div></div>
<div class="content">
<div class="heading"><div><h2>Maintenance stays obvious</h2><p class="desc">The sidebar and focused tools make the flow clear without adding a separate wizard surface.</p></div></div>
<div class="workflow">
<div class="step"><span class="num">1</span><div><strong>Refresh cache</strong><div class="meta">Update changed files from configured roots.</div></div><button class="primary">Update 24h</button></div>
<div class="step"><span class="num">2</span><div><strong>Review skipped names</strong><div class="meta">Spot files that did not produce an ID.</div></div><button>Review Skipped</button></div>
<div class="step"><span class="num">3</span><div><strong>Review duplicates</strong><div class="meta">KEEP reasons and multipart-risk skips included.</div></div><button class="primary">Open Review</button></div>
<div class="step"><span class="num">4</span><div><strong>Check library issues</strong><div class="meta">Rename bracket/no-hyphen oddities if needed.</div></div><button>Open Issues</button></div>
</div>
<div class="panel" style="margin-top:9px;"><div class="chips"><span class="pill choice">Utility</span><button>Bulk ID Check</button><button>Cache Console</button><button>Ranking Rules</button></div></div>
</div>
</div>
<p class="verdict">Rejected as a dedicated home page. Useful order, unnecessary extra destination.</p>
</section>
<section class="option">
<div class="option-head"><div><h2>5. Rejected: Bulk Mode Inside Popup</h2><p class="option-note">This would turn the popup into a two-mode mini-app with cramped result review.</p></div><span class="pill choice">rejected</span></div>
<div class="popup-stage">
<div class="popup">
<div class="heading"><span class="brand">rclone-jav</span><div class="toolbar"><div class="seg"><span class="active">LIVE</span><span>CACHE</span></div><button>gear</button></div></div>
<div class="seg" style="margin-bottom:8px;"><span>Single</span><span class="active">Bulk</span></div>
<textarea>BLK-474&#10;FC2-PPV-1841460&#10;PRTD-[027-030]</textarea>
<div class="chips" style="margin:7px 0;"><button class="primary">Check IDs</button><button>Clear</button></div>
<div class="status">6 IDs checked · 4 match · 2 no match</div>
<div class="list">
<div class="row"><span class="path">BLK-474</span><span class="pill console">MATCH</span></div>
<div class="row"><span class="path">PRTD-029</span><span class="pill choice">NO MATCH</span></div>
<div class="row"><span class="path">FC2-PPV-1841460</span><span class="pill console">MATCH</span></div>
</div>
</div>
</div>
<p class="verdict">Rejected even for 5-20 IDs. The popup gets a doorway into a focused Bulk Check surface, not a permanent second mode.</p>
</section>
<section class="option">
<div class="option-head"><div><h2>6. Chosen: Bulk Check Quick Window</h2><p class="option-note">Typical batches are expected to be about 5-20 IDs, so the popup opens a compact focused window while the Console owns the full tool.</p></div><span class="pill console">chosen bulk path</span></div>
<div class="popup-stage">
<div class="three" style="width:100%; align-items:start;">
<div class="popup">
<div class="heading"><span class="brand">rclone-jav</span><button>gear</button></div>
<input value="BLK-474">
<div class="status" style="margin-top:8px;">MATCH · 1 hit</div>
<div class="chips" style="margin-top:8px;"><button>Re-Scan</button><button class="live">Bulk Check</button></div>
</div>
<div class="modal">
<div class="heading"><div><h2>Bulk Check</h2><p class="desc">Quick batch window</p></div><button>x</button></div>
<textarea>BLK-474&#10;FC2-PPV-1841460&#10;PRTD-027</textarea>
<div class="chips" style="margin-top:7px;"><button class="primary">Check IDs</button><button>Clear</button></div>
<div class="status" style="margin-top:8px;">3 IDs · 2 match · 1 no match</div>
<div class="list">
<div class="row"><span class="path">BLK-474</span><span class="pill console">MATCH</span></div>
<div class="row"><span class="path">PRTD-027</span><span class="pill choice">NO MATCH</span></div>
</div>
<div class="chips" style="margin-top:8px;"><button>Open Full Console</button></div>
</div>
<div class="card">
<h3>Console Owner</h3>
<textarea>Paste larger batches here...</textarea>
<div class="chips" style="margin-top:7px;"><button class="primary">Check IDs</button><button>Export Results</button></div>
<p class="meta" style="margin-top:7px;">Full-width rows, richer review, ranges, import/export, future filters.</p>
</div>
</div>
</div>
<p class="verdict">Chosen: popup opens a compact Bulk Check window for short batches. The Console remains the full batch-review surface.</p>
</section>
<section class="option">
<div class="option-head"><div><h2>7. Debug Split + Repo Preview</h2><p class="option-note">Debug history and standalone tests move out of daily workflow. Sim Dupe leaves the extension UI entirely.</p></div><span class="pill support">chosen support split</span></div>
<div class="mock">
<div class="top"><span class="brand">Support / Debug Tools</span><button>Diagnostics</button></div>
<div class="content debug-grid">
<div class="card">
<h3>Debug Tools</h3>
<div class="nav"><span class="active">Search Activity</span><span>Search Troubleshooting</span><span>ID Extraction</span><span>Page Extraction</span><span>Diagnostics</span></div>
</div>
<div class="preview">
<div class="heading"><div><h2>Repo Preview Harness</h2><p class="desc">Popup state samples live in a repo HTML file such as <span class="mono">samples/popup-states.html</span>, not as a hidden extension page.</p></div></div>
<div class="chips"><button>Open sample file</button><button class="live">Search Activity</button><button>Diagnostics</button></div>
<div class="preview-hit">
<div class="status">MATCH · sample popup state</div>
<div class="list"><div class="row"><span class="path">BLK-474 - ClearJAV.mp4</span><span>4.94 GiB</span></div><div class="row"><span class="path">BLK-474 [1080p].mp4</span><span>4.90 GiB</span></div></div>
</div>
</div>
</div>
</div>
<p class="verdict">Chosen: standalone support/debug tools remain available; Sim Dupe is removed from extension UI.</p>
</section>
<section class="option">
<div class="option-head"><div><h2>8. Inline Rule Feedback</h2><p class="option-note">Rule editors keep local feedback. Only standalone troubleshooting benches move to Debug Tools.</p></div><span class="pill support">chosen bench split</span></div>
<div class="mock">
<div class="top"><span class="brand">Settings / Matching Rules</span><span class="pill choice">feedback stays nearby</span></div>
<div class="content">
<div class="bench-switch"><span class="active">Custom Part Detector</span><span>ID Normalizer</span><span>Site Extraction</span></div>
<div class="two">
<div class="card">
<h3>Rule</h3>
<textarea>_PART(\d+)$</textarea>
<div class="chips" style="margin-top:7px;"><button class="primary">Test this rule</button><button>Use samples</button></div>
</div>
<div class="card">
<h3>Inline Feedback</h3>
<div class="list">
<div class="row"><span>KV-118_PART1.mp4</span><span class="pill settings">part 1</span></div>
<div class="row"><span>KV-118_PART2.mp4</span><span class="pill settings">part 2</span></div>
<div class="row"><span>Covered by built-in?</span><span class="pill console">shown</span></div>
</div>
</div>
</div>
<div class="panel" style="margin-top:9px;"><p class="meta">Standalone Search Troubleshooting, page extraction testing, and search history can still move into Support. Editing rules should not require leaving the editor to see feedback.</p></div>
</div>
</div>
<p class="verdict">Chosen: contextual inline tests stay. General troubleshooting tools move to Support.</p>
</section>
</div>
</main>
</body>
</html>
+2230
View File
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
import importlib.util
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("rcjav_rules", ROOT / "rc-jav.py")
RCJAV = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = RCJAV
SPEC.loader.exec_module(RCJAV)
def entry(path, size=1_000, jav_id="TEST-001"):
return RCJAV.FileEntry(
source="Target",
remote="cq:JAV",
path=path,
size=size,
mod_time="",
jav_id=jav_id,
)
class IdRuleTests(unittest.TestCase):
def test_builtin_multipart_shapes_keep_parts_distinct(self):
expected = {
"KV-118 - Aiba Reika_PART1.mp4": "KV-118#part1",
"KV-118_A.mp4": "KV-118#part1",
"OFJE-195-7 [480p].mp4": "OFJE-195#part7",
"ABC-027.mp4": "ABC-027",
}
for name, jav_id in expected.items():
with self.subTest(name=name):
self.assertEqual(RCJAV.extract_id(name), jav_id)
def test_multipart_files_do_not_form_base_duplicate_group(self):
files = [
entry("KV-118 - Aiba Reika_PART1.mp4", jav_id="KV-118"),
entry("KV-118 - Aiba Reika_PART2.mp4", jav_id="KV-118"),
entry("KV-118 - Aiba Reika_PART3.mp4", jav_id="KV-118"),
]
self.assertEqual(RCJAV.find_dupes(files), {})
def test_large_same_id_group_gets_manual_review_risk(self):
files = [
entry("TEST-001 direct.mp4"),
entry("TEST-001 edit.mp4"),
entry("TEST-001 mirror.mp4"),
]
risks = RCJAV.describe_dupe_risks("TEST-001", files)
self.assertIn("large_same_id_group", {risk["code"] for risk in risks})
class KeepRankingTests(unittest.TestCase):
def test_vip_folder_beats_larger_non_vip_copy(self):
keep, reason = RCJAV.decide_keep_with_reason([
entry("ClearJAV/TEST-001.mp4", size=2_000),
entry("Other/TEST-001 [1080p].mkv", size=9_000),
])
self.assertEqual(keep.path, "ClearJAV/TEST-001.mp4")
self.assertEqual(reason["code"], "vip_folder")
def test_ts_loses_to_non_ts_even_when_larger(self):
keep, reason = RCJAV.decide_keep_with_reason([
entry("Other/TEST-001.ts", size=9_000),
entry("Other/TEST-001.mp4", size=8_000),
])
self.assertEqual(keep.path, "Other/TEST-001.mp4")
self.assertEqual(reason["code"], "container")
if __name__ == "__main__":
unittest.main()