feat: add community publishing and news syndication
Validate and deploy production / validate (push) Successful in 23s
Validate and deploy staging / validate (push) Successful in 50s
Validate and deploy production / deploy (push) Failing after 40s
Validate and deploy staging / deploy (push) Failing after 7s

This commit is contained in:
jb
2026-08-01 21:42:09 -04:00
parent b47e2588e9
commit dd84dff055
288 changed files with 83254 additions and 38351 deletions
@@ -62,10 +62,17 @@
},
"unknown": {
"gear_slot": false,
"views": {"unknown": []}
"views": { "unknown": [] }
}
},
"gear_slots": ["Mask", "Gloves", "Backpack", "Body Armor", "Holster", "Knee Pads"],
"gear_slots": [
"Mask",
"Gloves",
"Backpack",
"Body Armor",
"Holster",
"Knee Pads"
],
"abstention_reasons": [
"low_confidence",
"ambiguous_family",
@@ -109,9 +116,20 @@
},
"extraction": {
"pending": ["local_processing", "cancelled"],
"local_processing": ["candidate_ready", "fallback_pending", "abstained", "failed", "cancelled"],
"local_processing": [
"candidate_ready",
"fallback_pending",
"abstained",
"failed",
"cancelled"
],
"fallback_pending": ["fallback_processing", "cancelled"],
"fallback_processing": ["candidate_ready", "abstained", "failed", "cancelled"],
"fallback_processing": [
"candidate_ready",
"abstained",
"failed",
"cancelled"
],
"candidate_ready": [],
"abstained": [],
"failed": ["pending"],
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,11 @@
"schema_version": 1,
"archive_id": "archive-001",
"created_at": "2026-07-31T12:00:00Z",
"generator": {"name": "game-data-capture", "version": "0.1.0", "policy_version": "1"},
"generator": {
"name": "game-data-capture",
"version": "0.1.0",
"policy_version": "1"
},
"artifacts": [
{
"artifact_id": "image-001",
@@ -15,7 +19,13 @@
}
],
"captures": [
{"capture_id": "capture-001", "artifact_id": "image-001", "state": "queued", "received_at": "2026-07-31T11:59:55Z", "failure_reason": null}
{
"capture_id": "capture-001",
"artifact_id": "image-001",
"state": "queued",
"received_at": "2026-07-31T11:59:55Z",
"failure_reason": null
}
],
"screens": [
{
@@ -36,7 +46,13 @@
"extractions": [],
"groups": [],
"reviews": [
{"review_id": "review-001", "candidate_id": "candidate-001", "state": "approved", "updated_at": "2026-07-31T12:00:00Z", "note": "Name verified against the image."}
{
"review_id": "review-001",
"candidate_id": "candidate-001",
"state": "approved",
"updated_at": "2026-07-31T12:00:00Z",
"note": "Name verified against the image."
}
],
"exports": [],
"errors": [],
@@ -48,10 +64,20 @@
"normalized_value": "LVOA-C",
"missing_reason": null,
"confidence": 0.99,
"source_engine": {"kind": "local_ocr", "engine_id": "fixture-ocr", "engine_version": "1"},
"source_engine": {
"kind": "local_ocr",
"engine_id": "fixture-ocr",
"engine_version": "1"
},
"image_artifact_id": "image-001",
"image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"region": {"coordinate_space": "normalized", "x": 0.1, "y": 0.1, "width": 0.3, "height": 0.08},
"region": {
"coordinate_space": "normalized",
"x": 0.1,
"y": 0.1,
"width": 0.3,
"height": 0.08
},
"correction_history": []
},
{
@@ -61,10 +87,20 @@
"normalized_value": 20,
"missing_reason": null,
"confidence": 0.98,
"source_engine": {"kind": "local_ocr", "engine_id": "fixture-ocr", "engine_version": "1"},
"source_engine": {
"kind": "local_ocr",
"engine_id": "fixture-ocr",
"engine_version": "1"
},
"image_artifact_id": "image-001",
"image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"region": {"coordinate_space": "pixels", "x": 50, "y": 50, "width": 80, "height": 30},
"region": {
"coordinate_space": "pixels",
"x": 50,
"y": 50,
"width": 80,
"height": 30
},
"correction_history": []
},
{
@@ -74,10 +110,20 @@
"normalized_value": "High-end",
"missing_reason": null,
"confidence": 0.96,
"source_engine": {"kind": "local_ocr", "engine_id": "fixture-ocr", "engine_version": "1"},
"source_engine": {
"kind": "local_ocr",
"engine_id": "fixture-ocr",
"engine_version": "1"
},
"image_artifact_id": "image-001",
"image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"region": {"coordinate_space": "pixels", "x": 50, "y": 85, "width": 140, "height": 30},
"region": {
"coordinate_space": "pixels",
"x": 50,
"y": 85,
"width": 140,
"height": 30
},
"correction_history": []
},
{
@@ -87,10 +133,20 @@
"normalized_value": 1,
"missing_reason": null,
"confidence": 0.97,
"source_engine": {"kind": "local_ocr", "engine_id": "fixture-ocr", "engine_version": "1"},
"source_engine": {
"kind": "local_ocr",
"engine_id": "fixture-ocr",
"engine_version": "1"
},
"image_artifact_id": "image-001",
"image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"region": {"coordinate_space": "pixels", "x": 200, "y": 85, "width": 60, "height": 30},
"region": {
"coordinate_space": "pixels",
"x": 200,
"y": 85,
"width": 60,
"height": 30
},
"correction_history": []
},
{
@@ -100,10 +156,20 @@
"normalized_value": null,
"missing_reason": "not_visible",
"confidence": 0,
"source_engine": {"kind": "manual", "engine_id": "review", "engine_version": "1"},
"source_engine": {
"kind": "manual",
"engine_id": "review",
"engine_version": "1"
},
"image_artifact_id": "image-001",
"image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"region": {"coordinate_space": "normalized", "x": 0, "y": 0, "width": 1, "height": 1},
"region": {
"coordinate_space": "normalized",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"correction_history": []
},
{
@@ -113,18 +179,36 @@
"normalized_value": 12400,
"missing_reason": null,
"confidence": 1,
"source_engine": {"kind": "manual", "engine_id": "review", "engine_version": "1"},
"source_engine": {
"kind": "manual",
"engine_id": "review",
"engine_version": "1"
},
"image_artifact_id": "image-001",
"image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"region": {"coordinate_space": "pixels", "x": 300, "y": 200, "width": 160, "height": 40},
"region": {
"coordinate_space": "pixels",
"x": 300,
"y": 200,
"width": 160,
"height": 40
},
"correction_history": [
{
"correction_id": "correction-001",
"corrected_at": "2026-07-31T12:00:00Z",
"actor": "local_user",
"reason": "Corrected a missing zero.",
"before": {"raw_text": "1,240", "normalized_value": 1240, "missing_reason": null},
"after": {"raw_text": "12,400", "normalized_value": 12400, "missing_reason": null}
"before": {
"raw_text": "1,240",
"normalized_value": 1240,
"missing_reason": null
},
"after": {
"raw_text": "12,400",
"normalized_value": 12400,
"missing_reason": null
}
}
]
}
@@ -135,13 +219,43 @@
"family": "weapon",
"source_screen_ids": ["screen-001"],
"observed_context": {
"level": {"path": "observed.level", "value": 20, "missing_reason": null, "evidence_refs": ["evidence-level"], "uncertainty_note": null},
"quality": {"path": "observed.quality", "value": "High-end", "missing_reason": null, "evidence_refs": ["evidence-quality"], "uncertainty_note": null},
"tier": {"path": "observed.tier", "value": 1, "missing_reason": null, "evidence_refs": ["evidence-tier"], "uncertainty_note": null},
"upgrade_state": {"path": "observed.upgrade_state", "value": null, "missing_reason": "not_visible", "evidence_refs": ["evidence-upgrade"], "uncertainty_note": "The screen does not show an upgrade indicator."}
"level": {
"path": "observed.level",
"value": 20,
"missing_reason": null,
"evidence_refs": ["evidence-level"],
"uncertainty_note": null
},
"quality": {
"path": "observed.quality",
"value": "High-end",
"missing_reason": null,
"evidence_refs": ["evidence-quality"],
"uncertainty_note": null
},
"tier": {
"path": "observed.tier",
"value": 1,
"missing_reason": null,
"evidence_refs": ["evidence-tier"],
"uncertainty_note": null
},
"upgrade_state": {
"path": "observed.upgrade_state",
"value": null,
"missing_reason": "not_visible",
"evidence_refs": ["evidence-upgrade"],
"uncertainty_note": "The screen does not show an upgrade indicator."
}
},
"fields": [
{"path": "observed.damage", "value": 12400, "missing_reason": null, "evidence_refs": ["evidence-damage"], "uncertainty_note": "This is the configured level-20 instance value, not a canonical range endpoint."}
{
"path": "observed.damage",
"value": 12400,
"missing_reason": null,
"evidence_refs": ["evidence-damage"],
"uncertainty_note": "This is the configured level-20 instance value, not a canonical range endpoint."
}
]
}
],
@@ -152,7 +266,13 @@
"source_instance_ids": ["instance-001"],
"evidence_refs": ["evidence-name"],
"fields": [
{"path": "weapon.name", "value": "LVOA-C", "missing_reason": null, "evidence_refs": ["evidence-name"], "uncertainty_note": null}
{
"path": "weapon.name",
"value": "LVOA-C",
"missing_reason": null,
"evidence_refs": ["evidence-name"],
"uncertainty_note": null
}
],
"completeness": "incomplete",
"relationship_status": "unresolved",
@@ -30,10 +30,20 @@
"normalized_value": "LVOA-C",
"missing_reason": null,
"confidence": 0.99,
"source_engine": {"kind": "local_ocr", "engine_id": "fixture-ocr", "engine_version": "1"},
"source_engine": {
"kind": "local_ocr",
"engine_id": "fixture-ocr",
"engine_version": "1"
},
"image_artifact_id": "image-001",
"image_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"region": {"coordinate_space": "normalized", "x": 0.1, "y": 0.1, "width": 0.3, "height": 0.08},
"region": {
"coordinate_space": "normalized",
"x": 0.1,
"y": 0.1,
"width": 0.3,
"height": 0.08
},
"correction_history": []
}
],
@@ -47,10 +57,24 @@
"source_candidate_ids": ["candidate-001"],
"observed_instance_ids": ["instance-001"],
"fields": [
{"path": "weapon.name", "value": "LVOA-C", "missing_reason": null, "evidence_refs": ["evidence-name"], "uncertainty_note": null}
{
"path": "weapon.name",
"value": "LVOA-C",
"missing_reason": null,
"evidence_refs": ["evidence-name"],
"uncertainty_note": null
}
],
"relationships": {"status": "unresolved", "evidence_refs": [], "note": "ISAC must resolve and validate canonical relationships before submission."},
"mechanics": {"status": "not_applicable", "evidence_refs": [], "note": null}
"relationships": {
"status": "unresolved",
"evidence_refs": [],
"note": "ISAC must resolve and validate canonical relationships before submission."
},
"mechanics": {
"status": "not_applicable",
"evidence_refs": [],
"note": null
}
}
]
}
@@ -5,191 +5,407 @@
"type": "object",
"additionalProperties": false,
"required": [
"schema_version", "handoff_id", "created_at", "source_archive", "trust_boundary",
"source_artifacts", "evidence_values", "records"
"schema_version",
"handoff_id",
"created_at",
"source_archive",
"trust_boundary",
"source_artifacts",
"evidence_values",
"records"
],
"properties": {
"schema_version": {"const": 1},
"handoff_id": {"$ref": "#/$defs/id"},
"created_at": {"$ref": "#/$defs/timestamp"},
"schema_version": { "const": 1 },
"handoff_id": { "$ref": "#/$defs/id" },
"created_at": { "$ref": "#/$defs/timestamp" },
"source_archive": {
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": ["archive_id", "manifest_sha256", "schema_version"],
"properties": {
"archive_id": {"$ref": "#/$defs/id"}, "manifest_sha256": {"$ref": "#/$defs/sha256"},
"schema_version": {"const": 1}
"archive_id": { "$ref": "#/$defs/id" },
"manifest_sha256": { "$ref": "#/$defs/sha256" },
"schema_version": { "const": 1 }
}
},
"trust_boundary": {
"type": "object", "additionalProperties": false,
"required": ["intended_destination", "mutation_authority", "requires_authenticated_action", "preserves_moderation", "creates_audit_events"],
"type": "object",
"additionalProperties": false,
"required": [
"intended_destination",
"mutation_authority",
"requires_authenticated_action",
"preserves_moderation",
"creates_audit_events"
],
"properties": {
"intended_destination": {"const": "authenticated_isac_intake"},
"mutation_authority": {"const": "none"},
"requires_authenticated_action": {"const": true},
"preserves_moderation": {"const": true},
"creates_audit_events": {"const": false}
"intended_destination": { "const": "authenticated_isac_intake" },
"mutation_authority": { "const": "none" },
"requires_authenticated_action": { "const": true },
"preserves_moderation": { "const": true },
"creates_audit_events": { "const": false }
}
},
"source_artifacts": {"type": "array", "maxItems": 10000, "items": {"$ref": "#/$defs/sourceArtifact"}},
"evidence_values": {"type": "array", "maxItems": 100000, "items": {"$ref": "#/$defs/evidenceValue"}},
"records": {"type": "array", "maxItems": 20000, "items": {"$ref": "#/$defs/handoffRecord"}}
"source_artifacts": {
"type": "array",
"maxItems": 10000,
"items": { "$ref": "#/$defs/sourceArtifact" }
},
"evidence_values": {
"type": "array",
"maxItems": 100000,
"items": { "$ref": "#/$defs/evidenceValue" }
},
"records": {
"type": "array",
"maxItems": 20000,
"items": { "$ref": "#/$defs/handoffRecord" }
}
},
"$defs": {
"id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"},
"sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"timestamp": {"type": "string", "format": "date-time", "maxLength": 40},
"recordType": {"enum": [
"weapon", "weapon_talent", "weapon_augmentation_profile", "gear", "brand_set",
"gear_talent", "gear_augmentation_profile", "skill_chip_set", "skill_chip_set_bonus",
"skill_chip_roll_pool", "skill_chip_talent", "os_protocol", "os_protocol_secondary_profile",
"os_protocol_talent", "specialization", "specialization_focus", "specialization_ability",
"specialization_tree_node", "specialization_tree_rank", "weapon_mod", "gear_mod"
]},
"missingReason": {"enum": [
"not_visible", "occluded", "unreadable", "not_present_on_screen", "unsupported",
"ambiguous", "extraction_failed", "not_applicable", "redacted"
]},
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"timestamp": { "type": "string", "format": "date-time", "maxLength": 40 },
"recordType": {
"enum": [
"weapon",
"weapon_talent",
"weapon_augmentation_profile",
"gear",
"brand_set",
"gear_talent",
"gear_augmentation_profile",
"skill_chip_set",
"skill_chip_set_bonus",
"skill_chip_roll_pool",
"skill_chip_talent",
"os_protocol",
"os_protocol_secondary_profile",
"os_protocol_talent",
"specialization",
"specialization_focus",
"specialization_ability",
"specialization_tree_node",
"specialization_tree_rank",
"weapon_mod",
"gear_mod"
]
},
"missingReason": {
"enum": [
"not_visible",
"occluded",
"unreadable",
"not_present_on_screen",
"unsupported",
"ambiguous",
"extraction_failed",
"not_applicable",
"redacted"
]
},
"jsonValue": {
"oneOf": [
{"type": "null"}, {"type": "boolean"}, {"type": "number"},
{"type": "string", "maxLength": 8192},
{"type": "array", "maxItems": 500, "items": {"$ref": "#/$defs/jsonValue"}},
{"type": "object", "maxProperties": 500, "additionalProperties": {"$ref": "#/$defs/jsonValue"}}
{ "type": "null" },
{ "type": "boolean" },
{ "type": "number" },
{ "type": "string", "maxLength": 8192 },
{
"type": "array",
"maxItems": 500,
"items": { "$ref": "#/$defs/jsonValue" }
},
{
"type": "object",
"maxProperties": 500,
"additionalProperties": { "$ref": "#/$defs/jsonValue" }
}
]
},
"sourceEngine": {
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": ["kind", "engine_id", "engine_version"],
"properties": {
"kind": {"enum": ["local_ocr", "openai_vision", "classifier", "manual", "video_decoder"]},
"engine_id": {"type": "string", "minLength": 1, "maxLength": 100},
"engine_version": {"type": "string", "minLength": 1, "maxLength": 100}
"kind": {
"enum": [
"local_ocr",
"openai_vision",
"classifier",
"manual",
"video_decoder"
]
},
"engine_id": { "type": "string", "minLength": 1, "maxLength": 100 },
"engine_version": { "type": "string", "minLength": 1, "maxLength": 100 }
}
},
"region": {
"oneOf": [
{
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": ["coordinate_space", "x", "y", "width", "height"],
"properties": {
"coordinate_space": {"const": "normalized"},
"x": {"type": "number", "minimum": 0, "maximum": 1},
"y": {"type": "number", "minimum": 0, "maximum": 1},
"width": {"type": "number", "exclusiveMinimum": 0, "maximum": 1},
"height": {"type": "number", "exclusiveMinimum": 0, "maximum": 1}
"coordinate_space": { "const": "normalized" },
"x": { "type": "number", "minimum": 0, "maximum": 1 },
"y": { "type": "number", "minimum": 0, "maximum": 1 },
"width": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
"height": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }
}
},
{
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": ["coordinate_space", "x", "y", "width", "height"],
"properties": {
"coordinate_space": {"const": "pixels"},
"x": {"type": "integer", "minimum": 0, "maximum": 100000},
"y": {"type": "integer", "minimum": 0, "maximum": 100000},
"width": {"type": "integer", "minimum": 1, "maximum": 100000},
"height": {"type": "integer", "minimum": 1, "maximum": 100000}
"coordinate_space": { "const": "pixels" },
"x": { "type": "integer", "minimum": 0, "maximum": 100000 },
"y": { "type": "integer", "minimum": 0, "maximum": 100000 },
"width": { "type": "integer", "minimum": 1, "maximum": 100000 },
"height": { "type": "integer", "minimum": 1, "maximum": 100000 }
}
}
]
},
"valueState": {
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": ["raw_text", "normalized_value", "missing_reason"],
"properties": {
"raw_text": {"type": ["string", "null"], "maxLength": 8192},
"normalized_value": {"$ref": "#/$defs/jsonValue"},
"missing_reason": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/missingReason"}]}
"raw_text": { "type": ["string", "null"], "maxLength": 8192 },
"normalized_value": { "$ref": "#/$defs/jsonValue" },
"missing_reason": {
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/missingReason" }]
}
},
"oneOf": [
{"properties": {"normalized_value": {"type": "null"}, "missing_reason": {"$ref": "#/$defs/missingReason"}}},
{"properties": {"normalized_value": {"not": {"type": "null"}}, "missing_reason": {"type": "null"}}}
{
"properties": {
"normalized_value": { "type": "null" },
"missing_reason": { "$ref": "#/$defs/missingReason" }
}
},
{
"properties": {
"normalized_value": { "not": { "type": "null" } },
"missing_reason": { "type": "null" }
}
}
]
},
"correction": {
"type": "object", "additionalProperties": false,
"required": ["correction_id", "corrected_at", "actor", "reason", "before", "after"],
"type": "object",
"additionalProperties": false,
"required": [
"correction_id",
"corrected_at",
"actor",
"reason",
"before",
"after"
],
"properties": {
"correction_id": {"$ref": "#/$defs/id"}, "corrected_at": {"$ref": "#/$defs/timestamp"},
"actor": {"enum": ["local_user", "system_reprocess"]},
"reason": {"type": "string", "minLength": 1, "maxLength": 1000},
"before": {"$ref": "#/$defs/valueState"}, "after": {"$ref": "#/$defs/valueState"}
"correction_id": { "$ref": "#/$defs/id" },
"corrected_at": { "$ref": "#/$defs/timestamp" },
"actor": { "enum": ["local_user", "system_reprocess"] },
"reason": { "type": "string", "minLength": 1, "maxLength": 1000 },
"before": { "$ref": "#/$defs/valueState" },
"after": { "$ref": "#/$defs/valueState" }
}
},
"evidenceValue": {
"type": "object", "additionalProperties": false,
"required": ["evidence_id", "field_path", "raw_text", "normalized_value", "missing_reason", "confidence", "source_engine", "image_artifact_id", "image_sha256", "region", "correction_history"],
"type": "object",
"additionalProperties": false,
"required": [
"evidence_id",
"field_path",
"raw_text",
"normalized_value",
"missing_reason",
"confidence",
"source_engine",
"image_artifact_id",
"image_sha256",
"region",
"correction_history"
],
"properties": {
"evidence_id": {"$ref": "#/$defs/id"},
"field_path": {"type": "string", "pattern": "^[A-Za-z0-9_.-]{1,256}$"},
"raw_text": {"type": ["string", "null"], "maxLength": 8192},
"normalized_value": {"$ref": "#/$defs/jsonValue"},
"missing_reason": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/missingReason"}]},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"source_engine": {"$ref": "#/$defs/sourceEngine"},
"image_artifact_id": {"$ref": "#/$defs/id"}, "image_sha256": {"$ref": "#/$defs/sha256"},
"region": {"$ref": "#/$defs/region"},
"correction_history": {"type": "array", "maxItems": 100, "items": {"$ref": "#/$defs/correction"}}
"evidence_id": { "$ref": "#/$defs/id" },
"field_path": {
"type": "string",
"pattern": "^[A-Za-z0-9_.-]{1,256}$"
},
"raw_text": { "type": ["string", "null"], "maxLength": 8192 },
"normalized_value": { "$ref": "#/$defs/jsonValue" },
"missing_reason": {
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/missingReason" }]
},
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"source_engine": { "$ref": "#/$defs/sourceEngine" },
"image_artifact_id": { "$ref": "#/$defs/id" },
"image_sha256": { "$ref": "#/$defs/sha256" },
"region": { "$ref": "#/$defs/region" },
"correction_history": {
"type": "array",
"maxItems": 100,
"items": { "$ref": "#/$defs/correction" }
}
},
"oneOf": [
{"properties": {"normalized_value": {"type": "null"}, "missing_reason": {"$ref": "#/$defs/missingReason"}}},
{"properties": {"normalized_value": {"not": {"type": "null"}}, "missing_reason": {"type": "null"}}}
{
"properties": {
"normalized_value": { "type": "null" },
"missing_reason": { "$ref": "#/$defs/missingReason" }
}
},
{
"properties": {
"normalized_value": { "not": { "type": "null" } },
"missing_reason": { "type": "null" }
}
}
]
},
"fieldClaim": {
"type": "object", "additionalProperties": false,
"required": ["path", "value", "missing_reason", "evidence_refs", "uncertainty_note"],
"type": "object",
"additionalProperties": false,
"required": [
"path",
"value",
"missing_reason",
"evidence_refs",
"uncertainty_note"
],
"properties": {
"path": {"type": "string", "pattern": "^[A-Za-z0-9_.-]{1,256}$"},
"value": {"$ref": "#/$defs/jsonValue"},
"missing_reason": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/missingReason"}]},
"evidence_refs": {"type": "array", "minItems": 1, "maxItems": 100, "uniqueItems": true, "items": {"$ref": "#/$defs/id"}},
"uncertainty_note": {"type": ["string", "null"], "maxLength": 2000}
"path": { "type": "string", "pattern": "^[A-Za-z0-9_.-]{1,256}$" },
"value": { "$ref": "#/$defs/jsonValue" },
"missing_reason": {
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/missingReason" }]
},
"evidence_refs": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"uniqueItems": true,
"items": { "$ref": "#/$defs/id" }
},
"uncertainty_note": { "type": ["string", "null"], "maxLength": 2000 }
},
"oneOf": [
{"properties": {"value": {"type": "null"}, "missing_reason": {"$ref": "#/$defs/missingReason"}}},
{"properties": {"value": {"not": {"type": "null"}}, "missing_reason": {"type": "null"}}}
{
"properties": {
"value": { "type": "null" },
"missing_reason": { "$ref": "#/$defs/missingReason" }
}
},
{
"properties": {
"value": { "not": { "type": "null" } },
"missing_reason": { "type": "null" }
}
}
]
},
"sourceArtifact": {
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": ["artifact_id", "sha256", "media_type", "byte_size"],
"properties": {
"artifact_id": {"$ref": "#/$defs/id"}, "sha256": {"$ref": "#/$defs/sha256"},
"media_type": {"enum": ["image/png", "image/webp", "image/avif"]},
"byte_size": {"type": "integer", "minimum": 1, "maximum": 67108864}
"artifact_id": { "$ref": "#/$defs/id" },
"sha256": { "$ref": "#/$defs/sha256" },
"media_type": { "enum": ["image/png", "image/webp", "image/avif"] },
"byte_size": { "type": "integer", "minimum": 1, "maximum": 67108864 }
}
},
"evidenceSection": {
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": ["status", "evidence_refs", "note"],
"properties": {
"status": {"enum": ["evidenced", "unresolved", "unsupported", "not_applicable"]},
"evidence_refs": {"type": "array", "maxItems": 1000, "uniqueItems": true, "items": {"$ref": "#/$defs/id"}},
"note": {"type": ["string", "null"], "maxLength": 4000}
"status": {
"enum": ["evidenced", "unresolved", "unsupported", "not_applicable"]
},
"evidence_refs": {
"type": "array",
"maxItems": 1000,
"uniqueItems": true,
"items": { "$ref": "#/$defs/id" }
},
"note": { "type": ["string", "null"], "maxLength": 4000 }
}
},
"handoffRecord": {
"type": "object", "additionalProperties": false,
"type": "object",
"additionalProperties": false,
"required": [
"handoff_record_id", "record_type", "intent", "target_id", "review_state",
"source_candidate_ids", "observed_instance_ids", "fields", "relationships", "mechanics"
"handoff_record_id",
"record_type",
"intent",
"target_id",
"review_state",
"source_candidate_ids",
"observed_instance_ids",
"fields",
"relationships",
"mechanics"
],
"properties": {
"handoff_record_id": {"$ref": "#/$defs/id"}, "record_type": {"$ref": "#/$defs/recordType"},
"intent": {"enum": ["create_draft", "update_draft", "abstain"]},
"target_id": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/id"}]},
"review_state": {"enum": ["approved", "abstained"]},
"source_candidate_ids": {"type": "array", "minItems": 1, "maxItems": 100, "uniqueItems": true, "items": {"$ref": "#/$defs/id"}},
"observed_instance_ids": {"type": "array", "minItems": 1, "maxItems": 100, "uniqueItems": true, "items": {"$ref": "#/$defs/id"}},
"fields": {"type": "array", "maxItems": 1000, "items": {"$ref": "#/$defs/fieldClaim"}},
"relationships": {"$ref": "#/$defs/evidenceSection"},
"mechanics": {"$ref": "#/$defs/evidenceSection"}
"handoff_record_id": { "$ref": "#/$defs/id" },
"record_type": { "$ref": "#/$defs/recordType" },
"intent": { "enum": ["create_draft", "update_draft", "abstain"] },
"target_id": {
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/id" }]
},
"review_state": { "enum": ["approved", "abstained"] },
"source_candidate_ids": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"uniqueItems": true,
"items": { "$ref": "#/$defs/id" }
},
"observed_instance_ids": {
"type": "array",
"minItems": 1,
"maxItems": 100,
"uniqueItems": true,
"items": { "$ref": "#/$defs/id" }
},
"fields": {
"type": "array",
"maxItems": 1000,
"items": { "$ref": "#/$defs/fieldClaim" }
},
"relationships": { "$ref": "#/$defs/evidenceSection" },
"mechanics": { "$ref": "#/$defs/evidenceSection" }
},
"oneOf": [
{"properties": {"intent": {"const": "create_draft"}, "target_id": {"const": null}, "review_state": {"const": "approved"}}},
{"properties": {"intent": {"const": "update_draft"}, "target_id": {"$ref": "#/$defs/id"}, "review_state": {"const": "approved"}}},
{"properties": {"intent": {"const": "abstain"}, "target_id": {"const": null}, "review_state": {"const": "abstained"}, "fields": {"maxItems": 0}}}
{
"properties": {
"intent": { "const": "create_draft" },
"target_id": { "const": null },
"review_state": { "const": "approved" }
}
},
{
"properties": {
"intent": { "const": "update_draft" },
"target_id": { "$ref": "#/$defs/id" },
"review_state": { "const": "approved" }
}
},
{
"properties": {
"intent": { "const": "abstain" },
"target_id": { "const": null },
"review_state": { "const": "abstained" },
"fields": { "maxItems": 0 }
}
}
]
}
}
@@ -4,86 +4,202 @@
"title": "Screenshot classification v1",
"type": "object",
"additionalProperties": false,
"required": ["family", "view", "gear_slot", "record_types", "confidence", "abstention_reason"],
"required": [
"family",
"view",
"gear_slot",
"record_types",
"confidence",
"abstention_reason"
],
"properties": {
"family": {"enum": ["weapon", "gear", "skill_chip", "os_protocol", "specialization", "weapon_mod", "gear_mod", "unknown"]},
"view": {"type": "string"},
"gear_slot": {"type": ["string", "null"]},
"record_types": {"type": "array", "uniqueItems": true, "maxItems": 8, "items": {"type": "string"}},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"abstention_reason": {"type": ["string", "null"]}
"family": {
"enum": [
"weapon",
"gear",
"skill_chip",
"os_protocol",
"specialization",
"weapon_mod",
"gear_mod",
"unknown"
]
},
"view": { "type": "string" },
"gear_slot": { "type": ["string", "null"] },
"record_types": {
"type": "array",
"uniqueItems": true,
"maxItems": 8,
"items": { "type": "string" }
},
"confidence": { "type": "number", "minimum": 0, "maximum": 1 },
"abstention_reason": { "type": ["string", "null"] }
},
"oneOf": [
{
"properties": {
"family": {"const": "weapon"},
"view": {"enum": ["detail", "blueprint", "crafting", "stat_range", "talent_grid", "augmentation"]},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "items": {"enum": ["weapon", "weapon_talent", "weapon_augmentation_profile"]}},
"abstention_reason": {"const": null}
"family": { "const": "weapon" },
"view": {
"enum": [
"detail",
"blueprint",
"crafting",
"stat_range",
"talent_grid",
"augmentation"
]
},
"gear_slot": { "const": null },
"record_types": {
"minItems": 1,
"items": {
"enum": ["weapon", "weapon_talent", "weapon_augmentation_profile"]
}
},
"abstention_reason": { "const": null }
}
},
{
"properties": {
"family": {"const": "gear"},
"view": {"enum": ["detail", "blueprint", "talent", "augmentation"]},
"gear_slot": {"enum": ["Mask", "Gloves", "Backpack", "Body Armor", "Holster", "Knee Pads"]},
"record_types": {"minItems": 1, "items": {"enum": ["gear", "brand_set", "gear_talent", "gear_augmentation_profile"]}},
"abstention_reason": {"const": null}
"family": { "const": "gear" },
"view": { "enum": ["detail", "blueprint", "talent", "augmentation"] },
"gear_slot": {
"enum": [
"Mask",
"Gloves",
"Backpack",
"Body Armor",
"Holster",
"Knee Pads"
]
},
"record_types": {
"minItems": 1,
"items": {
"enum": [
"gear",
"brand_set",
"gear_talent",
"gear_augmentation_profile"
]
}
},
"abstention_reason": { "const": null }
}
},
{
"properties": {
"family": {"const": "skill_chip"},
"view": {"enum": ["detail", "set_bonus", "talent"]},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "items": {"enum": ["skill_chip_set", "skill_chip_set_bonus", "skill_chip_roll_pool", "skill_chip_talent"]}},
"abstention_reason": {"const": null}
"family": { "const": "skill_chip" },
"view": { "enum": ["detail", "set_bonus", "talent"] },
"gear_slot": { "const": null },
"record_types": {
"minItems": 1,
"items": {
"enum": [
"skill_chip_set",
"skill_chip_set_bonus",
"skill_chip_roll_pool",
"skill_chip_talent"
]
}
},
"abstention_reason": { "const": null }
}
},
{
"properties": {
"family": {"const": "os_protocol"},
"view": {"enum": ["detail", "secondary_profile", "talent"]},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "items": {"enum": ["os_protocol", "os_protocol_secondary_profile", "os_protocol_talent"]}},
"abstention_reason": {"const": null}
"family": { "const": "os_protocol" },
"view": { "enum": ["detail", "secondary_profile", "talent"] },
"gear_slot": { "const": null },
"record_types": {
"minItems": 1,
"items": {
"enum": [
"os_protocol",
"os_protocol_secondary_profile",
"os_protocol_talent"
]
}
},
"abstention_reason": { "const": null }
}
},
{
"properties": {
"family": {"const": "specialization"},
"view": {"enum": ["overview", "details", "ability", "focus_details", "focus_tree", "rank_details"]},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "items": {"enum": ["specialization", "specialization_focus", "specialization_ability", "specialization_tree_node", "specialization_tree_rank"]}},
"abstention_reason": {"const": null}
"family": { "const": "specialization" },
"view": {
"enum": [
"overview",
"details",
"ability",
"focus_details",
"focus_tree",
"rank_details"
]
},
"gear_slot": { "const": null },
"record_types": {
"minItems": 1,
"items": {
"enum": [
"specialization",
"specialization_focus",
"specialization_ability",
"specialization_tree_node",
"specialization_tree_rank"
]
}
},
"abstention_reason": { "const": null }
}
},
{
"properties": {
"family": {"const": "weapon_mod"},
"view": {"const": "detail"},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "maxItems": 1, "items": {"const": "weapon_mod"}},
"abstention_reason": {"const": null}
"family": { "const": "weapon_mod" },
"view": { "const": "detail" },
"gear_slot": { "const": null },
"record_types": {
"minItems": 1,
"maxItems": 1,
"items": { "const": "weapon_mod" }
},
"abstention_reason": { "const": null }
}
},
{
"properties": {
"family": {"const": "gear_mod"},
"view": {"const": "detail"},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "maxItems": 1, "items": {"const": "gear_mod"}},
"abstention_reason": {"const": null}
"family": { "const": "gear_mod" },
"view": { "const": "detail" },
"gear_slot": { "const": null },
"record_types": {
"minItems": 1,
"maxItems": 1,
"items": { "const": "gear_mod" }
},
"abstention_reason": { "const": null }
}
},
{
"properties": {
"family": {"const": "unknown"},
"view": {"const": "unknown"},
"gear_slot": {"const": null},
"record_types": {"maxItems": 0},
"abstention_reason": {"enum": ["low_confidence", "ambiguous_family", "ambiguous_view", "unsupported_family", "unsupported_view", "insufficient_visual_evidence", "occluded", "corrupt_image", "resource_limit", "cancelled"]}
"family": { "const": "unknown" },
"view": { "const": "unknown" },
"gear_slot": { "const": null },
"record_types": { "maxItems": 0 },
"abstention_reason": {
"enum": [
"low_confidence",
"ambiguous_family",
"ambiguous_view",
"unsupported_family",
"unsupported_view",
"insufficient_visual_evidence",
"occluded",
"corrupt_image",
"resource_limit",
"cancelled"
]
}
}
}
]
@@ -20,111 +20,467 @@
background: var(--canvas);
}
* { box-sizing: border-box; }
* {
box-sizing: border-box;
}
body { margin: 0; background: var(--canvas); color: var(--text); }
body {
margin: 0;
background: var(--canvas);
color: var(--text);
}
button, input { font: inherit; }
button,
input {
font: inherit;
}
button, input[type="radio"], input[type="checkbox"] { accent-color: var(--orange); }
button,
input[type='radio'],
input[type='checkbox'] {
accent-color: var(--orange);
}
button { min-height: 44px; border: 1px solid var(--line-strong); border-radius: 0; background: var(--surface); color: var(--text); cursor: pointer; padding: .65rem .9rem; font-weight: 700; }
button:hover:not(:disabled) { background: #1b2021; border-color: var(--text); }
button:active:not(:disabled) { transform: translateY(1px); }
button:disabled { color: var(--dim); cursor: not-allowed; border-color: var(--line); background: #101213; }
.button-primary { background: var(--orange); color: #12100b; border-color: var(--orange); }
.button-primary:hover:not(:disabled) { background: var(--orange-hover); border-color: var(--orange-hover); }
.button-danger { color: var(--danger); }
.quiet-danger { border-color: #6f4941; }
button {
min-height: 44px;
border: 1px solid var(--line-strong);
border-radius: 0;
background: var(--surface);
color: var(--text);
cursor: pointer;
padding: 0.65rem 0.9rem;
font-weight: 700;
}
button:hover:not(:disabled) {
background: #1b2021;
border-color: var(--text);
}
button:active:not(:disabled) {
transform: translateY(1px);
}
button:disabled {
color: var(--dim);
cursor: not-allowed;
border-color: var(--line);
background: #101213;
}
.button-primary {
background: var(--orange);
color: #12100b;
border-color: var(--orange);
}
.button-primary:hover:not(:disabled) {
background: var(--orange-hover);
border-color: var(--orange-hover);
}
.button-danger {
color: var(--danger);
}
.quiet-danger {
border-color: #6f4941;
}
:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; }
:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 3px;
}
.skip-link { position: fixed; z-index: 2; top: 0; left: .5rem; transform: translateY(-120%); background: var(--orange); color: #12100b; padding: .65rem; font-weight: 700; }
.skip-link:focus { transform: translateY(0); }
.skip-link {
position: fixed;
z-index: 2;
top: 0;
left: 0.5rem;
transform: translateY(-120%);
background: var(--orange);
color: #12100b;
padding: 0.65rem;
font-weight: 700;
}
.skip-link:focus {
transform: translateY(0);
}
.shell { width: min(100% - 2rem, 100rem); margin: 0 auto; padding: 1.5rem 0 3rem; }
.command-header, .control-strip, .panel-heading { display: flex; gap: var(--space); justify-content: space-between; align-items: center; }
.command-header { border-bottom: 1px solid var(--line); padding: 0 0 1.25rem; }
.kicker { margin: 0 0 .35rem; color: var(--muted); font-size: .74rem; font-weight: 800; letter-spacing: .08em; }
h1, h2, p { margin-top: 0; }
h1 { margin-bottom: 0; font-size: clamp(1.65rem, 3vw, 2.35rem); letter-spacing: -.025em; }
h2 { margin-bottom: 0; font-size: 1.15rem; }
.header-state { text-align: right; }
.header-state strong, #capture-state, #review-state { display: block; font-size: .88rem; font-weight: 800; letter-spacing: .04em; }
.header-state span, #queue-state, #capture-count, .field-help, .action-help, figcaption { color: var(--muted); font-size: .88rem; }
.shell {
width: min(100% - 2rem, 100rem);
margin: 0 auto;
padding: 1.5rem 0 3rem;
}
.command-header,
.control-strip,
.panel-heading {
display: flex;
gap: var(--space);
justify-content: space-between;
align-items: center;
}
.command-header {
border-bottom: 1px solid var(--line);
padding: 0 0 1.25rem;
}
.kicker {
margin: 0 0 0.35rem;
color: var(--muted);
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.08em;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 0;
font-size: clamp(1.65rem, 3vw, 2.35rem);
letter-spacing: -0.025em;
}
h2 {
margin-bottom: 0;
font-size: 1.15rem;
}
.header-state {
text-align: right;
}
.header-state strong,
#capture-state,
#review-state {
display: block;
font-size: 0.88rem;
font-weight: 800;
letter-spacing: 0.04em;
}
.header-state span,
#queue-state,
#capture-count,
.field-help,
.action-help,
figcaption {
color: var(--muted);
font-size: 0.88rem;
}
.notice { display: flex; gap: .75rem; border: 1px solid var(--line); border-left: 2px solid var(--info); margin: 1rem 0; padding: .8rem 1rem; background: var(--well); }
.notice strong { color: var(--info); font-size: .78rem; letter-spacing: .06em; }
.notice[data-kind="success"] { border-left-color: var(--success); }
.notice[data-kind="success"] strong { color: var(--success); }
.notice[data-kind="error"], .notice[data-kind="conflict"] { border-left-color: var(--danger); }
.notice[data-kind="error"] strong, .notice[data-kind="conflict"] strong { color: var(--danger); }
.notice[data-kind="retained"] { border-left-color: var(--info); }
.notice[data-kind="retained"] strong { color: var(--info); }
.notice {
display: flex;
gap: 0.75rem;
border: 1px solid var(--line);
border-left: 2px solid var(--info);
margin: 1rem 0;
padding: 0.8rem 1rem;
background: var(--well);
}
.notice strong {
color: var(--info);
font-size: 0.78rem;
letter-spacing: 0.06em;
}
.notice[data-kind='success'] {
border-left-color: var(--success);
}
.notice[data-kind='success'] strong {
color: var(--success);
}
.notice[data-kind='error'],
.notice[data-kind='conflict'] {
border-left-color: var(--danger);
}
.notice[data-kind='error'] strong,
.notice[data-kind='conflict'] strong {
color: var(--danger);
}
.notice[data-kind='retained'] {
border-left-color: var(--info);
}
.notice[data-kind='retained'] strong {
color: var(--info);
}
.control-strip { border: 1px solid var(--line); background: var(--surface-raised); padding: 1rem; margin-bottom: 1rem; }
.control-strip p:last-child { margin: 0; }
.control-actions, .form-actions, .dialog-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.control-strip {
border: 1px solid var(--line);
background: var(--surface-raised);
padding: 1rem;
margin-bottom: 1rem;
}
.control-strip p:last-child {
margin: 0;
}
.control-actions,
.form-actions,
.dialog-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.workspace-grid { display: grid; grid-template-columns: minmax(17rem, 24rem) minmax(0, 1fr); border: 1px solid var(--line); background: var(--surface); }
.capture-panel { border-right: 1px solid var(--line); display: flex; flex-direction: column; height: min(42rem, calc(100vh - 10rem)); min-height: 24rem; min-width: 0; }
.capture-panel, .review-panel { padding: 1rem; }
.panel-heading { border-bottom: 1px solid var(--line); padding-bottom: .85rem; }
.queue-filter { display: flex; gap: .55rem; align-items: center; min-height: 44px; color: var(--muted); font-size: .88rem; }
.queue-filter input { inline-size: 1.1rem; block-size: 1.1rem; flex: 0 0 auto; }
.capture-scroll { min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding-right: .25rem; scrollbar-gutter: stable; }
.empty-copy, .review-empty { color: var(--muted); padding: 1rem 0; line-height: 1.5; }
.capture-list { margin: 0; padding: 0; list-style: none; }
.capture-list button { width: 100%; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .25rem .6rem; margin-top: .55rem; text-align: left; background: var(--well); }
.capture-list button[aria-current="true"] { border-color: var(--orange); color: var(--text); }
.capture-list .capture-id { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.capture-list .capture-meta { display: grid; gap: .15rem; color: var(--muted); font-size: .72rem; line-height: 1.2; text-align: right; white-space: nowrap; }
.capture-list .capture-review { color: var(--text); font-size: .75rem; font-weight: 800; letter-spacing: .04em; }
.capture-list .capture-time { overflow: hidden; text-overflow: ellipsis; }
.workspace-grid {
display: grid;
grid-template-columns: minmax(17rem, 24rem) minmax(0, 1fr);
border: 1px solid var(--line);
background: var(--surface);
}
.capture-panel {
border-right: 1px solid var(--line);
display: flex;
flex-direction: column;
height: min(42rem, calc(100vh - 10rem));
min-height: 24rem;
min-width: 0;
}
.capture-panel,
.review-panel {
padding: 1rem;
}
.panel-heading {
border-bottom: 1px solid var(--line);
padding-bottom: 0.85rem;
}
.queue-filter {
display: flex;
gap: 0.55rem;
align-items: center;
min-height: 44px;
color: var(--muted);
font-size: 0.88rem;
}
.queue-filter input {
inline-size: 1.1rem;
block-size: 1.1rem;
flex: 0 0 auto;
}
.capture-scroll {
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
padding-right: 0.25rem;
scrollbar-gutter: stable;
}
.empty-copy,
.review-empty {
color: var(--muted);
padding: 1rem 0;
line-height: 1.5;
}
.capture-list {
margin: 0;
padding: 0;
list-style: none;
}
.capture-list button {
width: 100%;
min-width: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.25rem 0.6rem;
margin-top: 0.55rem;
text-align: left;
background: var(--well);
}
.capture-list button[aria-current='true'] {
border-color: var(--orange);
color: var(--text);
}
.capture-list .capture-id {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.capture-list .capture-meta {
display: grid;
gap: 0.15rem;
color: var(--muted);
font-size: 0.72rem;
line-height: 1.2;
text-align: right;
white-space: nowrap;
}
.capture-list .capture-review {
color: var(--text);
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.04em;
}
.capture-list .capture-time {
overflow: hidden;
text-overflow: ellipsis;
}
.review-content { padding-top: 1rem; }
.evidence-frame { margin: 0 0 1rem; border: 1px solid var(--line); background: var(--well); padding: .65rem; }
.evidence-frame img { display: block; width: 100%; max-height: 31rem; object-fit: contain; background: #000; }
.evidence-frame figcaption { padding-top: .65rem; overflow-wrap: anywhere; }
fieldset { margin: 0 0 1rem; padding: .75rem; border: 1px solid var(--line); min-width: 0; }
fieldset:disabled { opacity: .65; }
legend { padding: 0 .35rem; font-weight: 700; }
.choice-grid { display: flex; flex-wrap: wrap; gap: .45rem; }
.choice { position: relative; display: inline-flex; min-height: 44px; align-items: center; border: 1px solid var(--line-strong); background: var(--well); cursor: pointer; }
.choice:hover { border-color: var(--text); }
.choice input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.choice span { padding: .65rem .75rem; }
.choice input:checked + span { background: #3c2a0c; color: var(--text); box-shadow: inset 0 -2px var(--orange); }
.choice input:focus-visible + span { outline: 3px solid var(--focus); outline-offset: 2px; }
.validation-message { min-height: 1.5rem; margin: -.35rem 0 .75rem; color: var(--danger); }
.action-help { max-width: 70ch; margin: .8rem 0 0; line-height: 1.45; }
.review-content {
padding-top: 1rem;
}
.evidence-frame {
margin: 0 0 1rem;
border: 1px solid var(--line);
background: var(--well);
padding: 0.65rem;
}
.evidence-frame img {
display: block;
width: 100%;
max-height: 31rem;
object-fit: contain;
background: #000;
}
.evidence-frame figcaption {
padding-top: 0.65rem;
overflow-wrap: anywhere;
}
fieldset {
margin: 0 0 1rem;
padding: 0.75rem;
border: 1px solid var(--line);
min-width: 0;
}
fieldset:disabled {
opacity: 0.65;
}
legend {
padding: 0 0.35rem;
font-weight: 700;
}
.choice-grid {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.choice {
position: relative;
display: inline-flex;
min-height: 44px;
align-items: center;
border: 1px solid var(--line-strong);
background: var(--well);
cursor: pointer;
}
.choice:hover {
border-color: var(--text);
}
.choice input {
position: absolute;
opacity: 0;
inset: 0;
cursor: pointer;
}
.choice span {
padding: 0.65rem 0.75rem;
}
.choice input:checked + span {
background: #3c2a0c;
color: var(--text);
box-shadow: inset 0 -2px var(--orange);
}
.choice input:focus-visible + span {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
.validation-message {
min-height: 1.5rem;
margin: -0.35rem 0 0.75rem;
color: var(--danger);
}
.action-help {
max-width: 70ch;
margin: 0.8rem 0 0;
line-height: 1.45;
}
dialog { width: min(calc(100% - 2rem), 34rem); border: 1px solid var(--orange); border-radius: 0; padding: 1.3rem; color: var(--text); background: var(--surface-raised); box-shadow: 0 18px 60px #000; }
dialog::backdrop { background: rgb(0 0 0 / 72%); }
dialog p { line-height: 1.5; }
dialog {
width: min(calc(100% - 2rem), 34rem);
border: 1px solid var(--orange);
border-radius: 0;
padding: 1.3rem;
color: var(--text);
background: var(--surface-raised);
box-shadow: 0 18px 60px #000;
}
dialog::backdrop {
background: rgb(0 0 0 / 72%);
}
dialog p {
line-height: 1.5;
}
@media (max-width: 52rem) {
.shell { width: min(100% - 1rem, 100rem); padding-top: 1rem; }
.command-header, .control-strip { align-items: flex-start; flex-direction: column; }
.header-state { text-align: left; }
.workspace-grid { grid-template-columns: 1fr; }
.capture-panel { height: auto; min-height: 0; border-right: 0; border-bottom: 1px solid var(--line); }
.capture-scroll { overflow: visible; padding-right: 0; scrollbar-gutter: auto; }
.capture-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: .55rem; }
.capture-list button { margin-top: 0; }
.shell {
width: min(100% - 1rem, 100rem);
padding-top: 1rem;
}
.command-header,
.control-strip {
align-items: flex-start;
flex-direction: column;
}
.header-state {
text-align: left;
}
.workspace-grid {
grid-template-columns: 1fr;
}
.capture-panel {
height: auto;
min-height: 0;
border-right: 0;
border-bottom: 1px solid var(--line);
}
.capture-scroll {
overflow: visible;
padding-right: 0;
scrollbar-gutter: auto;
}
.capture-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 0.55rem;
}
.capture-list button {
margin-top: 0;
}
}
@media (max-width: 33rem) {
.control-actions, .form-actions, .dialog-actions { display: grid; grid-template-columns: 1fr; width: 100%; }
.control-actions button, .form-actions button, .dialog-actions button { width: 100%; }
.notice { align-items: flex-start; flex-direction: column; gap: .25rem; }
.control-actions,
.form-actions,
.dialog-actions {
display: grid;
grid-template-columns: 1fr;
width: 100%;
}
.control-actions button,
.form-actions button,
.dialog-actions button {
width: 100%;
}
.notice {
align-items: flex-start;
flex-direction: column;
gap: 0.25rem;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
@media (forced-colors: active) {
button, fieldset, .choice, .notice, .workspace-grid, .evidence-frame { border: 1px solid CanvasText; }
.button-primary, .choice input:checked + span { forced-color-adjust: none; background: Highlight; color: HighlightText; }
button,
fieldset,
.choice,
.notice,
.workspace-grid,
.evidence-frame {
border: 1px solid CanvasText;
}
.button-primary,
.choice input:checked + span {
forced-color-adjust: none;
background: Highlight;
color: HighlightText;
}
}
@@ -1,21 +1,132 @@
const taxonomy = Object.freeze({
weapon: { views: ["detail", "blueprint", "crafting", "stat_range", "talent_grid", "augmentation"], records: { detail: ["weapon", "weapon_talent"], blueprint: ["weapon", "weapon_talent"], crafting: ["weapon"], stat_range: ["weapon"], talent_grid: ["weapon_talent"], augmentation: ["weapon_augmentation_profile"] } },
gear: { views: ["detail", "blueprint", "talent", "augmentation"], records: { detail: ["gear", "brand_set"], blueprint: ["gear", "brand_set"], talent: ["gear_talent"], augmentation: ["gear_augmentation_profile"] } },
skill_chip: { views: ["detail", "set_bonus", "talent"], records: { detail: ["skill_chip_set", "skill_chip_roll_pool"], set_bonus: ["skill_chip_set_bonus"], talent: ["skill_chip_talent"] } },
os_protocol: { views: ["detail", "secondary_profile", "talent"], records: { detail: ["os_protocol"], secondary_profile: ["os_protocol_secondary_profile"], talent: ["os_protocol_talent"] } },
specialization: { views: ["overview", "details", "ability", "focus_details", "focus_tree", "rank_details"], records: { overview: ["specialization"], details: ["specialization"], ability: ["specialization_ability"], focus_details: ["specialization_focus"], focus_tree: ["specialization_focus", "specialization_tree_node"], rank_details: ["specialization_tree_rank"] } },
weapon_mod: { views: ["detail"], records: { detail: ["weapon_mod"] } },
gear_mod: { views: ["detail"], records: { detail: ["gear_mod"] } },
unknown: { views: ["unknown"], records: { unknown: [] } }
weapon: {
views: [
'detail',
'blueprint',
'crafting',
'stat_range',
'talent_grid',
'augmentation',
],
records: {
detail: ['weapon', 'weapon_talent'],
blueprint: ['weapon', 'weapon_talent'],
crafting: ['weapon'],
stat_range: ['weapon'],
talent_grid: ['weapon_talent'],
augmentation: ['weapon_augmentation_profile'],
},
},
gear: {
views: ['detail', 'blueprint', 'talent', 'augmentation'],
records: {
detail: ['gear', 'brand_set'],
blueprint: ['gear', 'brand_set'],
talent: ['gear_talent'],
augmentation: ['gear_augmentation_profile'],
},
},
skill_chip: {
views: ['detail', 'set_bonus', 'talent'],
records: {
detail: ['skill_chip_set', 'skill_chip_roll_pool'],
set_bonus: ['skill_chip_set_bonus'],
talent: ['skill_chip_talent'],
},
},
os_protocol: {
views: ['detail', 'secondary_profile', 'talent'],
records: {
detail: ['os_protocol'],
secondary_profile: ['os_protocol_secondary_profile'],
talent: ['os_protocol_talent'],
},
},
specialization: {
views: [
'overview',
'details',
'ability',
'focus_details',
'focus_tree',
'rank_details',
],
records: {
overview: ['specialization'],
details: ['specialization'],
ability: ['specialization_ability'],
focus_details: ['specialization_focus'],
focus_tree: ['specialization_focus', 'specialization_tree_node'],
rank_details: ['specialization_tree_rank'],
},
},
weapon_mod: { views: ['detail'], records: { detail: ['weapon_mod'] } },
gear_mod: { views: ['detail'], records: { detail: ['gear_mod'] } },
unknown: { views: ['unknown'], records: { unknown: [] } },
});
const families = Object.keys(taxonomy);
const gearSlots = ["Mask", "Gloves", "Backpack", "Body Armor", "Holster", "Knee Pads"];
const abstentionReasons = ["low_confidence", "ambiguous_family", "ambiguous_view", "unsupported_family", "unsupported_view", "insufficient_visual_evidence", "occluded", "corrupt_image", "resource_limit", "cancelled"];
const gearSlots = [
'Mask',
'Gloves',
'Backpack',
'Body Armor',
'Holster',
'Knee Pads',
];
const abstentionReasons = [
'low_confidence',
'ambiguous_family',
'ambiguous_view',
'unsupported_family',
'unsupported_view',
'insufficient_visual_evidence',
'occluded',
'corrupt_image',
'resource_limit',
'cancelled',
];
const csrfPattern = /^[A-Za-z0-9_-]{32,128}$/;
const $ = (id) => document.getElementById(id);
const els = Object.freeze({
sessionState: $("session-state"), queueState: $("queue-state"), captureState: $("capture-state"), notice: $("notice"), noticeTitle: $("notice-title"), noticeCopy: $("notice-copy"), start: $("start-button"), stop: $("stop-button"), refresh: $("refresh-button"), filter: $("hide-reviewed"), list: $("capture-list"), listEmpty: $("capture-empty"), count: $("capture-count"), reviewTitle: $("review-title"), reviewState: $("review-state"), reviewEmpty: $("review-empty"), reviewContent: $("review-content"), media: $("capture-media"), meta: $("capture-meta"), labelForm: $("label-form"), validation: $("label-validation"), save: $("save-label-button"), reject: $("reject-button"), remove: $("delete-button"), family: $("family-options"), view: $("view-options"), gear: $("gear-slot-options"), records: $("record-type-options"), abstention: $("abstention-options"), viewFieldset: $("view-fieldset"), gearFieldset: $("gear-slot-fieldset"), recordFieldset: $("record-types-fieldset"), abstentionFieldset: $("abstention-fieldset"), dialog: $("confirmation-dialog"), dialogKicker: $("confirmation-kicker"), dialogTitle: $("confirmation-title"), dialogCopy: $("confirmation-copy"), dialogSubmit: $("confirmation-submit")
sessionState: $('session-state'),
queueState: $('queue-state'),
captureState: $('capture-state'),
notice: $('notice'),
noticeTitle: $('notice-title'),
noticeCopy: $('notice-copy'),
start: $('start-button'),
stop: $('stop-button'),
refresh: $('refresh-button'),
filter: $('hide-reviewed'),
list: $('capture-list'),
listEmpty: $('capture-empty'),
count: $('capture-count'),
reviewTitle: $('review-title'),
reviewState: $('review-state'),
reviewEmpty: $('review-empty'),
reviewContent: $('review-content'),
media: $('capture-media'),
meta: $('capture-meta'),
labelForm: $('label-form'),
validation: $('label-validation'),
save: $('save-label-button'),
reject: $('reject-button'),
remove: $('delete-button'),
family: $('family-options'),
view: $('view-options'),
gear: $('gear-slot-options'),
records: $('record-type-options'),
abstention: $('abstention-options'),
viewFieldset: $('view-fieldset'),
gearFieldset: $('gear-slot-fieldset'),
recordFieldset: $('record-types-fieldset'),
abstentionFieldset: $('abstention-fieldset'),
dialog: $('confirmation-dialog'),
dialogKicker: $('confirmation-kicker'),
dialogTitle: $('confirmation-title'),
dialogCopy: $('confirmation-copy'),
dialogSubmit: $('confirmation-submit'),
});
let csrfToken = null;
@@ -26,31 +137,75 @@ let pending = false;
let returnFocus = null;
let hideReviewed = false;
function setText(element, value) { element.textContent = value; }
function clearNode(node) { while (node.firstChild) node.removeChild(node.firstChild); }
function label(value) { return value.replaceAll("_", " "); }
function notice(kind, title, copy) { els.notice.dataset.kind = kind; setText(els.noticeTitle, title); setText(els.noticeCopy, copy); }
function hasValidCsrf(value) { return typeof value === "string" && csrfPattern.test(value); }
function selected(name) { return document.querySelector(`input[name="${name}"]:checked`)?.value || null; }
function selectedAll(name) { return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map((input) => input.value); }
function currentFamily() { return selected("family"); }
function currentView() { return selected("view"); }
function isReviewedCapture(capture) { return capture?.classification !== null && capture?.classification !== undefined; }
function visibleCaptures() { return hideReviewed ? captures.filter((capture) => !isReviewedCapture(capture)) : captures; }
function abbreviateCaptureId(value) { return value.length <= 22 ? value : `${value.slice(0, 11)}…${value.slice(-8)}`; }
function setText(element, value) {
element.textContent = value;
}
function clearNode(node) {
while (node.firstChild) node.removeChild(node.firstChild);
}
function label(value) {
return value.replaceAll('_', ' ');
}
function notice(kind, title, copy) {
els.notice.dataset.kind = kind;
setText(els.noticeTitle, title);
setText(els.noticeCopy, copy);
}
function hasValidCsrf(value) {
return typeof value === 'string' && csrfPattern.test(value);
}
function selected(name) {
return document.querySelector(`input[name="${name}"]:checked`)?.value || null;
}
function selectedAll(name) {
return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map(
(input) => input.value,
);
}
function currentFamily() {
return selected('family');
}
function currentView() {
return selected('view');
}
function isReviewedCapture(capture) {
return (
capture?.classification !== null && capture?.classification !== undefined
);
}
function visibleCaptures() {
return hideReviewed
? captures.filter((capture) => !isReviewedCapture(capture))
: captures;
}
function abbreviateCaptureId(value) {
return value.length <= 22
? value
: `${value.slice(0, 11)}…${value.slice(-8)}`;
}
function formatCaptureTime(value) {
if (typeof value !== "string" || value.length > 64) return "TIME UNAVAILABLE";
if (typeof value !== 'string' || value.length > 64) return 'TIME UNAVAILABLE';
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp)) return "TIME UNAVAILABLE";
if (!Number.isFinite(timestamp)) return 'TIME UNAVAILABLE';
const date = new Date(timestamp);
const parts = [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes()];
if (parts.some((part) => !Number.isInteger(part))) return "TIME UNAVAILABLE";
const parts = [
date.getUTCFullYear(),
date.getUTCMonth() + 1,
date.getUTCDate(),
date.getUTCHours(),
date.getUTCMinutes(),
];
if (parts.some((part) => !Number.isInteger(part))) return 'TIME UNAVAILABLE';
const [year, month, day, hour, minute] = parts;
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")} ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} UTC`;
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')} UTC`;
}
function activeCaptureProjection() {
if (!activeCapture) return null;
return captures.find((capture) => capture.capture_id === activeCapture.capture_id) || activeCapture;
return (
captures.find(
(capture) => capture.capture_id === activeCapture.capture_id,
) || activeCapture
);
}
function syncControls() {
@@ -62,23 +217,30 @@ function syncControls() {
els.save.disabled = pending || locked || !activeCapture || !isValidLabel();
els.reject.disabled = pending || locked || !activeCapture;
els.remove.disabled = pending || locked || !activeCapture;
document.querySelectorAll("#capture-list button").forEach((button) => {
document.querySelectorAll('#capture-list button').forEach((button) => {
button.disabled = pending || locked;
});
document.querySelectorAll("#label-form input").forEach((input) => {
const fieldset = input.closest("fieldset");
input.disabled = pending || locked || !activeCapture || Boolean(fieldset?.disabled || fieldset?.hidden);
document.querySelectorAll('#label-form input').forEach((input) => {
const fieldset = input.closest('fieldset');
input.disabled =
pending ||
locked ||
!activeCapture ||
Boolean(fieldset?.disabled || fieldset?.hidden);
});
els.labelForm.setAttribute("aria-busy", String(pending));
els.labelForm.setAttribute('aria-busy', String(pending));
}
function setBusy(value) {
pending = value;
syncControls();
}
function setBusy(value) { pending = value; syncControls(); }
function clearActiveDetail() {
activeCapture = null;
els.media.removeAttribute("src");
els.media.removeAttribute('src');
els.reviewContent.hidden = true;
els.reviewEmpty.hidden = false;
els.reviewTitle.textContent = "No capture selected";
els.reviewState.textContent = csrfToken ? "READY" : "LOCKED";
els.reviewTitle.textContent = 'No capture selected';
els.reviewState.textContent = csrfToken ? 'READY' : 'LOCKED';
renderLabelForm(null);
}
function lock(message) {
@@ -86,56 +248,125 @@ function lock(message) {
armed = null;
captures = [];
clearActiveDetail();
els.sessionState.textContent = "LOCKED";
els.captureState.textContent = "DISARMED";
els.queueState.textContent = "Queue unavailable";
els.sessionState.textContent = 'LOCKED';
els.captureState.textContent = 'DISARMED';
els.queueState.textContent = 'Queue unavailable';
els.listEmpty.hidden = false;
setText(els.listEmpty, message);
clearNode(els.list);
els.count.textContent = "0 shown / 0 loaded";
els.count.textContent = '0 shown / 0 loaded';
setBusy(false);
}
function validSession(payload) {
return Boolean(payload) && hasValidCsrf(payload.csrf_token) && typeof payload.armed === "boolean" && Number.isInteger(payload.queue_depth) && Number.isInteger(payload.queue_capacity) && payload.queue_depth >= 0 && payload.queue_depth <= payload.queue_capacity && payload.queue_capacity <= 256;
return (
Boolean(payload) &&
hasValidCsrf(payload.csrf_token) &&
typeof payload.armed === 'boolean' &&
Number.isInteger(payload.queue_depth) &&
Number.isInteger(payload.queue_capacity) &&
payload.queue_depth >= 0 &&
payload.queue_depth <= payload.queue_capacity &&
payload.queue_capacity <= 256
);
}
function updateSession(payload) {
armed = payload.armed;
els.sessionState.textContent = "UNLOCKED";
els.captureState.textContent = armed ? "ARMED" : "DISARMED";
els.sessionState.textContent = 'UNLOCKED';
els.captureState.textContent = armed ? 'ARMED' : 'DISARMED';
els.queueState.textContent = `Queue ${payload.queue_depth} / ${payload.queue_capacity}`;
if (!activeCapture) els.reviewState.textContent = "READY";
if (!activeCapture) els.reviewState.textContent = 'READY';
syncControls();
}
function safeJson(response) { return response.json().catch(() => null); }
function safeJson(response) {
return response.json().catch(() => null);
}
async function request(path, options = {}, mutation = false) {
const headers = new Headers(options.headers || {});
if (mutation) { headers.set("Content-Type", "application/json"); headers.set("X-CSRF-Token", csrfToken || ""); }
if (mutation) {
headers.set('Content-Type', 'application/json');
headers.set('X-CSRF-Token', csrfToken || '');
}
let response;
try { response = await fetch(path, { ...options, headers, credentials: "same-origin" }); } catch { notice("error", "NETWORK ERROR", "The local service did not respond. Your current review remains unchanged."); return null; }
if (response.status === 401 || response.status === 403) { lock("Local session expired or restarted. Open the provided local address again."); notice("error", "SESSION LOCKED", "The local session is no longer available. No action was retried."); return null; }
try {
response = await fetch(path, {
...options,
headers,
credentials: 'same-origin',
});
} catch {
notice(
'error',
'NETWORK ERROR',
'The local service did not respond. Your current review remains unchanged.',
);
return null;
}
if (response.status === 401 || response.status === 403) {
lock(
'Local session expired or restarted. Open the provided local address again.',
);
notice(
'error',
'SESSION LOCKED',
'The local session is no longer available. No action was retried.',
);
return null;
}
const payload = await safeJson(response);
if (payload === null) { notice("error", "RESPONSE ERROR", "The local service returned an unusable response. Your current review remains unchanged."); return null; }
if (payload === null) {
notice(
'error',
'RESPONSE ERROR',
'The local service returned an unusable response. Your current review remains unchanged.',
);
return null;
}
if (!response.ok) {
const retained = response.status === 409 && payload.error === "retained";
notice(retained ? "retained" : response.status === 409 ? "conflict" : "error", retained ? "RETAINED" : response.status === 409 ? "CONFLICT" : "ACTION FAILED", retained ? "Retention policy kept this evidence. No deletion occurred." : "The local service could not complete that action.");
const retained = response.status === 409 && payload.error === 'retained';
notice(
retained ? 'retained' : response.status === 409 ? 'conflict' : 'error',
retained
? 'RETAINED'
: response.status === 409
? 'CONFLICT'
: 'ACTION FAILED',
retained
? 'Retention policy kept this evidence. No deletion occurred.'
: 'The local service could not complete that action.',
);
return null;
}
return payload;
}
function acceptMutationCsrf(payload) {
if (hasValidCsrf(payload?.csrf_token)) { csrfToken = payload.csrf_token; return true; }
lock("The local service returned an invalid session update. Open the provided local address again.");
notice("error", "SESSION LOCKED", "The confirmed action was not retried.");
if (hasValidCsrf(payload?.csrf_token)) {
csrfToken = payload.csrf_token;
return true;
}
lock(
'The local service returned an invalid session update. Open the provided local address again.',
);
notice('error', 'SESSION LOCKED', 'The confirmed action was not retried.');
return false;
}
function addChoice(container, type, name, value, checked, onChange) {
const wrapper = document.createElement("label"); wrapper.className = "choice";
const input = document.createElement("input"); input.type = type; input.name = name; input.value = value; input.checked = checked; input.addEventListener("change", onChange);
const text = document.createElement("span"); text.textContent = label(value);
wrapper.append(input, text); container.append(wrapper);
const wrapper = document.createElement('label');
wrapper.className = 'choice';
const input = document.createElement('input');
input.type = type;
input.name = name;
input.value = value;
input.checked = checked;
input.addEventListener('change', onChange);
const text = document.createElement('span');
text.textContent = label(value);
wrapper.append(input, text);
container.append(wrapper);
}
function setChecked(name, value) {
document.querySelectorAll(`input[name="${name}"]`).forEach((input) => { input.checked = input.value === value; });
document.querySelectorAll(`input[name="${name}"]`).forEach((input) => {
input.checked = input.value === value;
});
}
function setCheckedMembers(name, values) {
const selectedValues = new Set(values);
@@ -145,83 +376,177 @@ function setCheckedMembers(name, values) {
}
function renderFamilies() {
clearNode(els.family);
families.forEach((family) => addChoice(els.family, "radio", "family", family, false, () => { renderViews(); renderDependentFields(); }));
families.forEach((family) =>
addChoice(els.family, 'radio', 'family', family, false, () => {
renderViews();
renderDependentFields();
}),
);
}
function renderViews() {
const family = currentFamily();
clearNode(els.view);
els.viewFieldset.disabled = !family;
if (!family) return;
taxonomy[family].views.forEach((view) => addChoice(els.view, "radio", "view", view, false, renderDependentFields));
taxonomy[family].views.forEach((view) =>
addChoice(els.view, 'radio', 'view', view, false, renderDependentFields),
);
}
function renderDependentFields() {
const family = currentFamily(); const view = currentView(); const unknown = family === "unknown"; const isGear = family === "gear";
els.gearFieldset.hidden = !isGear; els.gearFieldset.disabled = !isGear;
els.abstentionFieldset.hidden = !unknown; els.abstentionFieldset.disabled = !unknown;
const family = currentFamily();
const view = currentView();
const unknown = family === 'unknown';
const isGear = family === 'gear';
els.gearFieldset.hidden = !isGear;
els.gearFieldset.disabled = !isGear;
els.abstentionFieldset.hidden = !unknown;
els.abstentionFieldset.disabled = !unknown;
els.recordFieldset.disabled = !family || !view || unknown;
clearNode(els.gear); clearNode(els.records); clearNode(els.abstention);
if (isGear) gearSlots.forEach((slot) => addChoice(els.gear, "radio", "gear_slot", slot, false, validateLabel));
if (unknown) abstentionReasons.forEach((reason) => addChoice(els.abstention, "radio", "abstention_reason", reason, false, validateLabel));
if (!unknown && family && view) (taxonomy[family].records[view] || []).forEach((record) => addChoice(els.records, "checkbox", "record_type", record, false, validateLabel));
clearNode(els.gear);
clearNode(els.records);
clearNode(els.abstention);
if (isGear)
gearSlots.forEach((slot) =>
addChoice(els.gear, 'radio', 'gear_slot', slot, false, validateLabel),
);
if (unknown)
abstentionReasons.forEach((reason) =>
addChoice(
els.abstention,
'radio',
'abstention_reason',
reason,
false,
validateLabel,
),
);
if (!unknown && family && view)
(taxonomy[family].records[view] || []).forEach((record) =>
addChoice(
els.records,
'checkbox',
'record_type',
record,
false,
validateLabel,
),
);
validateLabel();
}
function labelPayload() {
const family = currentFamily(); const view = currentView(); const unknown = family === "unknown";
return { family, view, gear_slot: family === "gear" ? selected("gear_slot") : null, record_types: unknown ? [] : selectedAll("record_type"), confidence: 1.0, abstention_reason: unknown ? selected("abstention_reason") : null };
const family = currentFamily();
const view = currentView();
const unknown = family === 'unknown';
return {
family,
view,
gear_slot: family === 'gear' ? selected('gear_slot') : null,
record_types: unknown ? [] : selectedAll('record_type'),
confidence: 1.0,
abstention_reason: unknown ? selected('abstention_reason') : null,
};
}
function isValidLabel() {
const payload = labelPayload();
if (!payload.family || !payload.view) return false;
if (payload.family === "unknown") return payload.view === "unknown" && !payload.gear_slot && payload.record_types.length === 0 && Boolean(payload.abstention_reason);
return payload.record_types.length > 0 && (payload.family !== "gear" || Boolean(payload.gear_slot));
if (payload.family === 'unknown')
return (
payload.view === 'unknown' &&
!payload.gear_slot &&
payload.record_types.length === 0 &&
Boolean(payload.abstention_reason)
);
return (
payload.record_types.length > 0 &&
(payload.family !== 'gear' || Boolean(payload.gear_slot))
);
}
function validateLabel() {
const payload = labelPayload(); let message = "";
if (!payload.family) message = "Select the evidence family.";
else if (!payload.view) message = "Select the screen view.";
else if (payload.family === "unknown" && !payload.abstention_reason) message = "Select an explicit abstention reason for an unknown screen.";
else if (payload.family === "gear" && !payload.gear_slot) message = "Select the Gear slot shown by this evidence.";
else if (payload.family !== "unknown" && payload.record_types.length === 0) message = "Select at least one compatible canonical record type.";
const payload = labelPayload();
let message = '';
if (!payload.family) message = 'Select the evidence family.';
else if (!payload.view) message = 'Select the screen view.';
else if (payload.family === 'unknown' && !payload.abstention_reason)
message = 'Select an explicit abstention reason for an unknown screen.';
else if (payload.family === 'gear' && !payload.gear_slot)
message = 'Select the Gear slot shown by this evidence.';
else if (payload.family !== 'unknown' && payload.record_types.length === 0)
message = 'Select at least one compatible canonical record type.';
setText(els.validation, message);
if (!pending) syncControls();
return !message;
}
function renderLabelForm(classification) {
renderFamilies();
if (!classification) { renderViews(); renderDependentFields(); return; }
setChecked("family", classification.family);
if (!classification) {
renderViews();
renderDependentFields();
return;
}
setChecked('family', classification.family);
renderViews();
setChecked("view", classification.view);
setChecked('view', classification.view);
renderDependentFields();
if (classification.gear_slot) setChecked("gear_slot", classification.gear_slot);
setCheckedMembers("record_type", classification.record_types || []);
if (classification.abstention_reason) setChecked("abstention_reason", classification.abstention_reason);
if (classification.gear_slot)
setChecked('gear_slot', classification.gear_slot);
setCheckedMembers('record_type', classification.record_types || []);
if (classification.abstention_reason)
setChecked('abstention_reason', classification.abstention_reason);
validateLabel();
}
function renderCaptures() {
const visible = visibleCaptures();
clearNode(els.list); els.count.textContent = `${visible.length} shown / ${captures.length} loaded`; els.listEmpty.hidden = visible.length > 0;
clearNode(els.list);
els.count.textContent = `${visible.length} shown / ${captures.length} loaded`;
els.listEmpty.hidden = visible.length > 0;
if (!visible.length) {
setText(els.listEmpty, captures.length ? "ALL LOADED CAPTURES HIDDEN: clear Hide reviewed captures to inspect loaded captures." : "NO LOADED CAPTURES: refresh to check the latest local queue.");
setText(
els.listEmpty,
captures.length
? 'ALL LOADED CAPTURES HIDDEN: clear Hide reviewed captures to inspect loaded captures.'
: 'NO LOADED CAPTURES: refresh to check the latest local queue.',
);
return;
}
visible.forEach((capture) => {
const item = document.createElement("li"); const button = document.createElement("button"); button.type = "button";
if (activeCapture?.capture_id === capture.capture_id) button.setAttribute("aria-current", "true");
const reviewState = isReviewedCapture(capture) ? "REVIEWED" : "UNREVIEWED";
const item = document.createElement('li');
const button = document.createElement('button');
button.type = 'button';
if (activeCapture?.capture_id === capture.capture_id)
button.setAttribute('aria-current', 'true');
const reviewState = isReviewedCapture(capture) ? 'REVIEWED' : 'UNREVIEWED';
const displayedTime = formatCaptureTime(capture.received_at);
button.setAttribute("aria-label", `Capture ${capture.capture_id}. ${reviewState}. ${displayedTime}.`);
const id = document.createElement("span"); id.className = "capture-id"; id.textContent = abbreviateCaptureId(capture.capture_id);
const meta = document.createElement("span"); meta.className = "capture-meta";
const review = document.createElement("span"); review.className = "capture-review"; review.textContent = reviewState;
const time = document.createElement("span"); time.className = "capture-time"; time.textContent = displayedTime;
button.setAttribute(
'aria-label',
`Capture ${capture.capture_id}. ${reviewState}. ${displayedTime}.`,
);
const id = document.createElement('span');
id.className = 'capture-id';
id.textContent = abbreviateCaptureId(capture.capture_id);
const meta = document.createElement('span');
meta.className = 'capture-meta';
const review = document.createElement('span');
review.className = 'capture-review';
review.textContent = reviewState;
const time = document.createElement('span');
time.className = 'capture-time';
time.textContent = displayedTime;
meta.append(review, time);
button.append(id, meta); button.addEventListener("click", () => selectCapture(capture.capture_id)); item.append(button); els.list.append(item);
button.append(id, meta);
button.addEventListener('click', () => selectCapture(capture.capture_id));
item.append(button);
els.list.append(item);
});
syncControls();
}
function focusIfAvailable(control) { if (control && !control.disabled) control.focus(); }
function focusVisibleCaptureOrFilter() { focusIfAvailable(els.list.querySelector("button:not(:disabled)") || els.filter); }
function focusIfAvailable(control) {
if (control && !control.disabled) control.focus();
}
function focusVisibleCaptureOrFilter() {
focusIfAvailable(
els.list.querySelector('button:not(:disabled)') || els.filter,
);
}
function applyReviewedFilter() {
hideReviewed = els.filter.checked;
const activeProjection = activeCaptureProjection();
@@ -232,103 +557,295 @@ function applyReviewedFilter() {
}
async function refreshCaptures({ announce = true, fallbackFocus = null } = {}) {
if (!csrfToken || pending) return false;
setBusy(true); if (announce) notice("info", "LOADING", "Refreshing the local capture queue.");
const payload = await request("/api/captures");
if (!payload || !Array.isArray(payload.captures)) { setBusy(false); if (announce) focusIfAvailable(fallbackFocus); return false; }
setBusy(true);
if (announce)
notice('info', 'LOADING', 'Refreshing the local capture queue.');
const payload = await request('/api/captures');
if (!payload || !Array.isArray(payload.captures)) {
setBusy(false);
if (announce) focusIfAvailable(fallbackFocus);
return false;
}
captures = payload.captures;
const activeProjection = activeCaptureProjection();
if (activeCapture && (!activeProjection || (hideReviewed && isReviewedCapture(activeProjection)))) clearActiveDetail();
renderCaptures(); if (announce) notice("success", "QUEUE REFRESHED", "Local capture list updated."); setBusy(false); if (announce) focusIfAvailable(fallbackFocus); return true;
if (
activeCapture &&
(!activeProjection || (hideReviewed && isReviewedCapture(activeProjection)))
)
clearActiveDetail();
renderCaptures();
if (announce)
notice('success', 'QUEUE REFRESHED', 'Local capture list updated.');
setBusy(false);
if (announce) focusIfAvailable(fallbackFocus);
return true;
}
async function selectCapture(captureId, { announce = true } = {}) {
if (!csrfToken || pending) return;
setBusy(true); if (announce) notice("info", "LOADING", "Loading selected private evidence.");
const payload = await request(`/api/captures/${encodeURIComponent(captureId)}`);
if (!payload?.capture) { setBusy(false); return; }
activeCapture = payload.capture; els.reviewContent.hidden = false; els.reviewEmpty.hidden = true; els.reviewTitle.textContent = activeCapture.capture_id; els.reviewState.textContent = activeCapture.state;
setBusy(true);
if (announce) notice('info', 'LOADING', 'Loading selected private evidence.');
const payload = await request(
`/api/captures/${encodeURIComponent(captureId)}`,
);
if (!payload?.capture) {
setBusy(false);
return;
}
activeCapture = payload.capture;
els.reviewContent.hidden = false;
els.reviewEmpty.hidden = true;
els.reviewTitle.textContent = activeCapture.capture_id;
els.reviewState.textContent = activeCapture.state;
els.meta.textContent = `State: ${activeCapture.state} · Received: ${activeCapture.received_at}`;
els.media.removeAttribute("src"); els.media.src = `/api/captures/${encodeURIComponent(activeCapture.capture_id)}/media`;
renderCaptures(); renderLabelForm(activeCapture.classification); if (announce) notice("success", "CAPTURE READY", "Review the evidence, then save a manual label or choose a separate evidence action."); setBusy(false);
els.media.removeAttribute('src');
els.media.src = `/api/captures/${encodeURIComponent(activeCapture.capture_id)}/media`;
renderCaptures();
renderLabelForm(activeCapture.classification);
if (announce)
notice(
'success',
'CAPTURE READY',
'Review the evidence, then save a manual label or choose a separate evidence action.',
);
setBusy(false);
}
function routeForAction(action) {
if (action === "start") return ["/api/session/start", "START"];
if (action === "stop") return ["/api/session/stop", "STOP"];
if ((action === "reject" || action === "delete") && activeCapture) return [`/api/captures/${encodeURIComponent(activeCapture.capture_id)}/${action}`, action === "reject" ? "REJECT" : "DELETE"];
if (action === 'start') return ['/api/session/start', 'START'];
if (action === 'stop') return ['/api/session/stop', 'STOP'];
if ((action === 'reject' || action === 'delete') && activeCapture)
return [
`/api/captures/${encodeURIComponent(activeCapture.capture_id)}/${action}`,
action === 'reject' ? 'REJECT' : 'DELETE',
];
return null;
}
function showConfirmation(action, source) {
if ((action === "reject" || action === "delete") && !activeCapture) { notice("error", "ACTION UNAVAILABLE", "Select a capture before confirming this evidence action."); return; }
const messages = { start: ["START SESSION", "Start capture now? This is the only action that can arm capture.", "Start Session"], stop: ["STOP SESSION", "Stop capture now? New screenshots will not enter the queue after the session stops.", "Stop Session"], reject: ["REJECT EVIDENCE", "Reject this evidence? The evidence remains retained for review and will not be deleted.", "Reject evidence"], delete: ["DELETE CAPTURE", "Request deletion of this capture? Retention policy may refuse deletion. Rejection does not delete evidence.", "Delete capture"] };
const message = messages[action]; if (!message) return;
returnFocus = source; els.dialog.returnValue = "cancel"; els.dialog.dataset.action = action;
els.dialogKicker.textContent = "EXPLICIT CONFIRMATION"; els.dialogTitle.textContent = message[0]; els.dialogCopy.textContent = message[1]; els.dialogSubmit.textContent = message[2]; els.dialogSubmit.className = action === "delete" || action === "reject" ? "button button-danger" : "button button-primary";
els.dialog.showModal(); els.dialogSubmit.focus();
if ((action === 'reject' || action === 'delete') && !activeCapture) {
notice(
'error',
'ACTION UNAVAILABLE',
'Select a capture before confirming this evidence action.',
);
return;
}
const messages = {
start: [
'START SESSION',
'Start capture now? This is the only action that can arm capture.',
'Start Session',
],
stop: [
'STOP SESSION',
'Stop capture now? New screenshots will not enter the queue after the session stops.',
'Stop Session',
],
reject: [
'REJECT EVIDENCE',
'Reject this evidence? The evidence remains retained for review and will not be deleted.',
'Reject evidence',
],
delete: [
'DELETE CAPTURE',
'Request deletion of this capture? Retention policy may refuse deletion. Rejection does not delete evidence.',
'Delete capture',
],
};
const message = messages[action];
if (!message) return;
returnFocus = source;
els.dialog.returnValue = 'cancel';
els.dialog.dataset.action = action;
els.dialogKicker.textContent = 'EXPLICIT CONFIRMATION';
els.dialogTitle.textContent = message[0];
els.dialogCopy.textContent = message[1];
els.dialogSubmit.textContent = message[2];
els.dialogSubmit.className =
action === 'delete' || action === 'reject'
? 'button button-danger'
: 'button button-primary';
els.dialog.showModal();
els.dialogSubmit.focus();
}
async function confirmed(action, fallbackFocus) {
if (pending) return;
const route = routeForAction(action);
if (!route) { notice("error", "ACTION UNAVAILABLE", "Select a capture before confirming this evidence action."); fallbackFocus?.focus(); return; }
setBusy(true); notice("info", "PENDING", "The local service is processing your confirmed action.");
const [path, confirmation] = route; const payload = await request(path, { method: "POST", body: JSON.stringify({ confirmation }) }, true);
if (!payload || !acceptMutationCsrf(payload)) { setBusy(false); fallbackFocus?.focus(); return; }
if ((action === "start" || action === "stop") && !validSession(payload)) { lock("The local service returned an invalid session update. Open the provided local address again."); setBusy(false); return; }
if (action === "delete") clearActiveDetail();
if (action === "start" || action === "stop") updateSession(payload);
const successFocus = action === "start" ? els.stop : action === "stop" ? els.start : action === "delete" ? els.refresh : fallbackFocus;
setBusy(false); if (!(await refreshCaptures({ announce: false }))) { focusIfAvailable(successFocus); return; }
notice("success", "SUCCESS", action === "reject" ? "Evidence rejected and retained." : action === "delete" ? "Deletion request completed." : `Capture session ${action === "start" ? "armed" : "disarmed"}.`);
if (!route) {
notice(
'error',
'ACTION UNAVAILABLE',
'Select a capture before confirming this evidence action.',
);
fallbackFocus?.focus();
return;
}
setBusy(true);
notice(
'info',
'PENDING',
'The local service is processing your confirmed action.',
);
const [path, confirmation] = route;
const payload = await request(
path,
{ method: 'POST', body: JSON.stringify({ confirmation }) },
true,
);
if (!payload || !acceptMutationCsrf(payload)) {
setBusy(false);
fallbackFocus?.focus();
return;
}
if ((action === 'start' || action === 'stop') && !validSession(payload)) {
lock(
'The local service returned an invalid session update. Open the provided local address again.',
);
setBusy(false);
return;
}
if (action === 'delete') clearActiveDetail();
if (action === 'start' || action === 'stop') updateSession(payload);
const successFocus =
action === 'start'
? els.stop
: action === 'stop'
? els.start
: action === 'delete'
? els.refresh
: fallbackFocus;
setBusy(false);
if (!(await refreshCaptures({ announce: false }))) {
focusIfAvailable(successFocus);
return;
}
notice(
'success',
'SUCCESS',
action === 'reject'
? 'Evidence rejected and retained.'
: action === 'delete'
? 'Deletion request completed.'
: `Capture session ${action === 'start' ? 'armed' : 'disarmed'}.`,
);
focusIfAvailable(successFocus);
}
async function initialize() {
els.filter.checked = false; hideReviewed = false;
els.filter.checked = false;
hideReviewed = false;
renderLabelForm(null);
let fragment = window.location.hash; let token = null;
if (fragment.length > 1 && fragment.length <= 256) { const params = new URLSearchParams(fragment.slice(1)); const candidate = params.get("bootstrap"); if (candidate && csrfPattern.test(candidate)) token = candidate; }
if (fragment) history.replaceState(null, "", `${window.location.pathname}${window.location.search}`);
fragment = "";
let fragment = window.location.hash;
let token = null;
if (fragment.length > 1 && fragment.length <= 256) {
const params = new URLSearchParams(fragment.slice(1));
const candidate = params.get('bootstrap');
if (candidate && csrfPattern.test(candidate)) token = candidate;
}
if (fragment)
history.replaceState(
null,
'',
`${window.location.pathname}${window.location.search}`,
);
fragment = '';
if (token) {
const bootstrap = await request("/api/bootstrap", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }) }); token = null;
if (!hasValidCsrf(bootstrap?.csrf_token)) { lock("The local service could not establish a safe session. Open the provided local address again."); return; }
const bootstrap = await request('/api/bootstrap', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token }),
});
token = null;
if (!hasValidCsrf(bootstrap?.csrf_token)) {
lock(
'The local service could not establish a safe session. Open the provided local address again.',
);
return;
}
csrfToken = bootstrap.csrf_token;
}
const session = await request("/api/session");
if (!validSession(session)) { lock("LOCKED: start the local service with its provided bootstrap address. This browser has no process-local session."); return; }
csrfToken = session.csrf_token; updateSession(session); if (!(await refreshCaptures({ announce: false }))) return;
const startupCopy = session.armed ? "Capture is ARMED. Stop Session requires explicit confirmation." : "Capture is DISARMED. Start Session requires explicit confirmation.";
notice("success", "LOCAL SESSION READY", startupCopy);
const session = await request('/api/session');
if (!validSession(session)) {
lock(
'LOCKED: start the local service with its provided bootstrap address. This browser has no process-local session.',
);
return;
}
csrfToken = session.csrf_token;
updateSession(session);
if (!(await refreshCaptures({ announce: false }))) return;
const startupCopy = session.armed
? 'Capture is ARMED. Stop Session requires explicit confirmation.'
: 'Capture is DISARMED. Start Session requires explicit confirmation.';
notice('success', 'LOCAL SESSION READY', startupCopy);
}
els.start.addEventListener("click", () => showConfirmation("start", els.start));
els.stop.addEventListener("click", () => showConfirmation("stop", els.stop));
els.refresh.addEventListener("click", () => refreshCaptures({ fallbackFocus: els.refresh }));
els.labelForm.addEventListener("submit", async (event) => {
els.start.addEventListener('click', () => showConfirmation('start', els.start));
els.stop.addEventListener('click', () => showConfirmation('stop', els.stop));
els.refresh.addEventListener('click', () =>
refreshCaptures({ fallbackFocus: els.refresh }),
);
els.labelForm.addEventListener('submit', async (event) => {
event.preventDefault();
if (!activeCapture || !validateLabel() || pending) return;
const captureId = activeCapture.capture_id;
setBusy(true); notice("info", "PENDING", "Saving the manual reviewed label.");
const payload = await request(`/api/captures/${encodeURIComponent(captureId)}/label`, { method: "POST", body: JSON.stringify(labelPayload()) }, true);
if (!payload || !acceptMutationCsrf(payload)) { setBusy(false); return; }
setBusy(true);
notice('info', 'PENDING', 'Saving the manual reviewed label.');
const payload = await request(
`/api/captures/${encodeURIComponent(captureId)}/label`,
{ method: 'POST', body: JSON.stringify(labelPayload()) },
true,
);
if (!payload || !acceptMutationCsrf(payload)) {
setBusy(false);
return;
}
setBusy(false);
if (!(await refreshCaptures({ announce: false }))) { focusIfAvailable(els.save); return; }
const savedCapture = captures.find((capture) => capture.capture_id === captureId);
const savedCaptureIsHidden = Boolean(savedCapture && hideReviewed && isReviewedCapture(savedCapture));
if (savedCapture && !savedCaptureIsHidden) await selectCapture(captureId, { announce: false });
else { clearActiveDetail(); renderCaptures(); }
notice("success", "LABEL SAVED", "Manual reviewed label saved with confidence 1.0.");
if (!(await refreshCaptures({ announce: false }))) {
focusIfAvailable(els.save);
return;
}
const savedCapture = captures.find(
(capture) => capture.capture_id === captureId,
);
const savedCaptureIsHidden = Boolean(
savedCapture && hideReviewed && isReviewedCapture(savedCapture),
);
if (savedCapture && !savedCaptureIsHidden)
await selectCapture(captureId, { announce: false });
else {
clearActiveDetail();
renderCaptures();
}
notice(
'success',
'LABEL SAVED',
'Manual reviewed label saved with confidence 1.0.',
);
if (savedCaptureIsHidden) focusVisibleCaptureOrFilter();
else focusIfAvailable(els.save);
});
els.filter.addEventListener("change", applyReviewedFilter);
els.reject.addEventListener("click", () => showConfirmation("reject", els.reject));
els.remove.addEventListener("click", () => showConfirmation("delete", els.remove));
els.dialog.addEventListener("close", () => {
const action = els.dialog.dataset.action; const shouldConfirm = els.dialog.returnValue === "confirm";
els.dialog.dataset.action = "";
els.filter.addEventListener('change', applyReviewedFilter);
els.reject.addEventListener('click', () =>
showConfirmation('reject', els.reject),
);
els.remove.addEventListener('click', () =>
showConfirmation('delete', els.remove),
);
els.dialog.addEventListener('close', () => {
const action = els.dialog.dataset.action;
const shouldConfirm = els.dialog.returnValue === 'confirm';
els.dialog.dataset.action = '';
const focusTarget = returnFocus;
if (focusTarget) focusTarget.focus();
returnFocus = null;
if (shouldConfirm && action) confirmed(action, focusTarget);
});
els.dialog.addEventListener("cancel", () => { els.dialog.returnValue = "cancel"; });
els.media.addEventListener("error", () => notice("error", "MEDIA UNAVAILABLE", "The private evidence image could not be loaded. Refresh captures before retrying."));
els.dialog.addEventListener('cancel', () => {
els.dialog.returnValue = 'cancel';
});
els.media.addEventListener('error', () =>
notice(
'error',
'MEDIA UNAVAILABLE',
'The private evidence image could not be loaded. Refresh captures before retrying.',
),
);
initialize();
@@ -33,29 +33,46 @@
<p id="capture-state">DISARMED</p>
</div>
<div class="control-actions">
<button class="button button-primary" id="start-button" type="button" disabled>Start Session</button>
<button class="button" id="stop-button" type="button" disabled>Stop Session</button>
<button class="button" id="refresh-button" type="button" disabled>Refresh captures</button>
<button
class="button button-primary"
id="start-button"
type="button"
disabled
>
Start Session
</button>
<button class="button" id="stop-button" type="button" disabled>
Stop Session
</button>
<button class="button" id="refresh-button" type="button" disabled>
Refresh captures
</button>
</div>
</section>
<section class="workspace-grid" aria-label="Capture review workspace">
<aside class="capture-panel" aria-labelledby="capture-list-title">
<div class="panel-heading">
<div class="panel-heading">
<div>
<p class="kicker">PRIVATE QUEUE</p>
<h2 id="capture-list-title">Captures</h2>
</div>
<span id="capture-count">0 shown / 0 loaded</span>
</div>
<label class="queue-filter" for="hide-reviewed">
<input id="hide-reviewed" type="checkbox" />
<span>Hide reviewed captures</span>
</label>
<div class="capture-scroll" id="capture-scroll">
<p class="empty-copy" id="capture-empty">Authenticate to inspect the local capture queue.</p>
<ol class="capture-list" id="capture-list" aria-label="Available captures"></ol>
</div>
<span id="capture-count">0 shown / 0 loaded</span>
</div>
<label class="queue-filter" for="hide-reviewed">
<input id="hide-reviewed" type="checkbox" />
<span>Hide reviewed captures</span>
</label>
<div class="capture-scroll" id="capture-scroll">
<p class="empty-copy" id="capture-empty">
Authenticate to inspect the local capture queue.
</p>
<ol
class="capture-list"
id="capture-list"
aria-label="Available captures"
></ol>
</div>
</aside>
<section class="review-panel" aria-labelledby="review-title">
@@ -68,7 +85,10 @@
</div>
<div class="review-empty" id="review-empty">
<p>Select a capture after the local session is unlocked. Media remains private to this browser session.</p>
<p>
Select a capture after the local session is unlocked. Media
remains private to this browser session.
</p>
</div>
<div class="review-content" id="review-content" hidden>
@@ -95,7 +115,9 @@
<fieldset id="record-types-fieldset" disabled>
<legend>Canonical records evidenced</legend>
<p class="field-help" id="record-types-help">Choose every record type shown by this screen.</p>
<p class="field-help" id="record-types-help">
Choose every record type shown by this screen.
</p>
<div class="choice-grid" id="record-type-options"></div>
</fieldset>
@@ -104,27 +126,73 @@
<div class="choice-grid" id="abstention-options"></div>
</fieldset>
<p class="validation-message" id="label-validation" role="status"></p>
<p
class="validation-message"
id="label-validation"
role="status"
></p>
<div class="form-actions">
<button class="button button-primary" id="save-label-button" type="submit" disabled>Save reviewed label</button>
<button class="button button-danger" id="reject-button" type="button" disabled>Reject evidence</button>
<button class="button button-danger quiet-danger" id="delete-button" type="button" disabled>Delete capture</button>
<button
class="button button-primary"
id="save-label-button"
type="submit"
disabled
>
Save reviewed label
</button>
<button
class="button button-danger"
id="reject-button"
type="button"
disabled
>
Reject evidence
</button>
<button
class="button button-danger quiet-danger"
id="delete-button"
type="button"
disabled
>
Delete capture
</button>
</div>
<p class="action-help">Rejecting retains evidence. Deletion requires separate confirmation and may be retained by policy.</p>
<p class="action-help">
Rejecting retains evidence. Deletion requires separate
confirmation and may be retained by policy.
</p>
</form>
</div>
</section>
</section>
</main>
<dialog class="confirmation-dialog" id="confirmation-dialog" aria-labelledby="confirmation-title">
<dialog
class="confirmation-dialog"
id="confirmation-dialog"
aria-labelledby="confirmation-title"
>
<form method="dialog" id="confirmation-form">
<p class="kicker" id="confirmation-kicker">CONFIRMATION REQUIRED</p>
<h2 id="confirmation-title">Confirm action</h2>
<p id="confirmation-copy"></p>
<div class="dialog-actions">
<button class="button" id="confirmation-cancel" value="cancel" type="submit">Cancel</button>
<button class="button button-primary" id="confirmation-submit" value="confirm" type="submit">Confirm</button>
<button
class="button"
id="confirmation-cancel"
value="cancel"
type="submit"
>
Cancel
</button>
<button
class="button button-primary"
id="confirmation-submit"
value="confirm"
type="submit"
>
Confirm
</button>
</div>
</form>
</dialog>