CODEX - Added Warning sign if Archetype is missing
This commit is contained in:
@@ -43,7 +43,7 @@ async def create_melody(
|
|||||||
publish: bool = Query(False),
|
publish: bool = Query(False),
|
||||||
_user: TokenPayload = Depends(require_permission("melodies", "add")),
|
_user: TokenPayload = Depends(require_permission("melodies", "add")),
|
||||||
):
|
):
|
||||||
return await service.create_melody(body, publish=publish)
|
return await service.create_melody(body, publish=publish, actor_name=_user.name)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{melody_id}", response_model=MelodyInDB)
|
@router.put("/{melody_id}", response_model=MelodyInDB)
|
||||||
@@ -52,7 +52,7 @@ async def update_melody(
|
|||||||
body: MelodyUpdate,
|
body: MelodyUpdate,
|
||||||
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
|
_user: TokenPayload = Depends(require_permission("melodies", "edit")),
|
||||||
):
|
):
|
||||||
return await service.update_melody(melody_id, body)
|
return await service.update_melody(melody_id, body, actor_name=_user.name)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{melody_id}", status_code=204)
|
@router.delete("/{melody_id}", status_code=204)
|
||||||
@@ -108,9 +108,9 @@ async def upload_file(
|
|||||||
name=melody.information.name,
|
name=melody.information.name,
|
||||||
previewURL=url,
|
previewURL=url,
|
||||||
)
|
)
|
||||||
))
|
), actor_name=_user.name)
|
||||||
elif file_type == "binary":
|
elif file_type == "binary":
|
||||||
await service.update_melody(melody_id, MelodyUpdate(url=url))
|
await service.update_melody(melody_id, MelodyUpdate(url=url), actor_name=_user.name)
|
||||||
|
|
||||||
return {"url": url, "file_type": file_type}
|
return {"url": url, "file_type": file_type}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from shared.firebase import get_db as get_firestore, get_bucket
|
from shared.firebase import get_db as get_firestore, get_bucket
|
||||||
from shared.exceptions import NotFoundError
|
from shared.exceptions import NotFoundError
|
||||||
@@ -93,10 +94,44 @@ async def get_melody(melody_id: str) -> MelodyInDB:
|
|||||||
raise NotFoundError("Melody")
|
raise NotFoundError("Melody")
|
||||||
|
|
||||||
|
|
||||||
async def create_melody(data: MelodyCreate, publish: bool = False) -> MelodyInDB:
|
def _sanitize_metadata_for_create(existing: dict | None, actor_name: str | None) -> dict:
|
||||||
|
now = datetime.utcnow().isoformat() + "Z"
|
||||||
|
metadata = dict(existing or {})
|
||||||
|
creator = metadata.get("createdBy") or actor_name or "Unknown"
|
||||||
|
created_at = metadata.get("dateCreated") or now
|
||||||
|
metadata["createdBy"] = creator
|
||||||
|
metadata["dateCreated"] = created_at
|
||||||
|
metadata["lastEditedBy"] = actor_name or metadata.get("lastEditedBy") or creator
|
||||||
|
metadata["dateEdited"] = now
|
||||||
|
if "adminNotes" not in metadata:
|
||||||
|
metadata["adminNotes"] = []
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_metadata_for_update(existing: dict | None, incoming: dict | None, actor_name: str | None) -> dict:
|
||||||
|
now = datetime.utcnow().isoformat() + "Z"
|
||||||
|
existing_meta = dict(existing or {})
|
||||||
|
incoming_meta = dict(incoming or {})
|
||||||
|
|
||||||
|
# Created fields are immutable after first set.
|
||||||
|
created_by = existing_meta.get("createdBy") or incoming_meta.get("createdBy") or actor_name or "Unknown"
|
||||||
|
date_created = existing_meta.get("dateCreated") or incoming_meta.get("dateCreated") or now
|
||||||
|
|
||||||
|
merged = {**existing_meta, **incoming_meta}
|
||||||
|
merged["createdBy"] = created_by
|
||||||
|
merged["dateCreated"] = date_created
|
||||||
|
merged["lastEditedBy"] = actor_name or incoming_meta.get("lastEditedBy") or existing_meta.get("lastEditedBy") or created_by
|
||||||
|
merged["dateEdited"] = now
|
||||||
|
if "adminNotes" not in merged:
|
||||||
|
merged["adminNotes"] = existing_meta.get("adminNotes", [])
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
async def create_melody(data: MelodyCreate, publish: bool = False, actor_name: str | None = None) -> MelodyInDB:
|
||||||
"""Create a new melody. If publish=True, also push to Firestore."""
|
"""Create a new melody. If publish=True, also push to Firestore."""
|
||||||
melody_id = str(uuid.uuid4())
|
melody_id = str(uuid.uuid4())
|
||||||
doc_data = data.model_dump()
|
doc_data = data.model_dump()
|
||||||
|
doc_data["metadata"] = _sanitize_metadata_for_create(doc_data.get("metadata"), actor_name)
|
||||||
status = "published" if publish else "draft"
|
status = "published" if publish else "draft"
|
||||||
|
|
||||||
# Always save to SQLite
|
# Always save to SQLite
|
||||||
@@ -110,7 +145,7 @@ async def create_melody(data: MelodyCreate, publish: bool = False) -> MelodyInDB
|
|||||||
return MelodyInDB(id=melody_id, status=status, **doc_data)
|
return MelodyInDB(id=melody_id, status=status, **doc_data)
|
||||||
|
|
||||||
|
|
||||||
async def update_melody(melody_id: str, data: MelodyUpdate) -> MelodyInDB:
|
async def update_melody(melody_id: str, data: MelodyUpdate, actor_name: str | None = None) -> MelodyInDB:
|
||||||
"""Update an existing melody. If published, also update Firestore."""
|
"""Update an existing melody. If published, also update Firestore."""
|
||||||
row = await melody_db.get_melody(melody_id)
|
row = await melody_db.get_melody(melody_id)
|
||||||
if not row:
|
if not row:
|
||||||
@@ -124,6 +159,12 @@ async def update_melody(melody_id: str, data: MelodyUpdate) -> MelodyInDB:
|
|||||||
if key in update_data and key in existing_data:
|
if key in update_data and key in existing_data:
|
||||||
merged = {**existing_data[key], **update_data[key]}
|
merged = {**existing_data[key], **update_data[key]}
|
||||||
update_data[key] = merged
|
update_data[key] = merged
|
||||||
|
if "metadata" in update_data or "metadata" in existing_data:
|
||||||
|
update_data["metadata"] = _sanitize_metadata_for_update(
|
||||||
|
existing_data.get("metadata"),
|
||||||
|
update_data.get("metadata"),
|
||||||
|
actor_name,
|
||||||
|
)
|
||||||
|
|
||||||
merged_data = {**existing_data, **update_data}
|
merged_data = {**existing_data, **update_data}
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export default function MelodyDetail() {
|
|||||||
const speedBpm = formatBpm(speedMs);
|
const speedBpm = formatBpm(speedMs);
|
||||||
const minBpm = formatBpm(info.minSpeed);
|
const minBpm = formatBpm(info.minSpeed);
|
||||||
const maxBpm = formatBpm(info.maxSpeed);
|
const maxBpm = formatBpm(info.maxSpeed);
|
||||||
|
const missingArchetype = Boolean(melody.pid) && !builtMelody?.id;
|
||||||
const languages = melodySettings?.available_languages || ["en"];
|
const languages = melodySettings?.available_languages || ["en"];
|
||||||
const displayName = getLocalizedValue(info.name, displayLang, "Untitled Melody");
|
const displayName = getLocalizedValue(info.name, displayLang, "Untitled Melody");
|
||||||
|
|
||||||
@@ -556,10 +557,6 @@ export default function MelodyDetail() {
|
|||||||
|
|
||||||
const handleDownload = async (e) => {
|
const handleDownload = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (binaryUrl.startsWith("http")) {
|
|
||||||
window.open(binaryUrl, "_blank", "noopener,noreferrer");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
let res = null;
|
let res = null;
|
||||||
@@ -586,7 +583,13 @@ export default function MelodyDetail() {
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.open(binaryUrl, "_blank", "noopener,noreferrer");
|
const a = document.createElement("a");
|
||||||
|
a.href = binaryUrl;
|
||||||
|
a.download = downloadName;
|
||||||
|
a.rel = "noopener noreferrer";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -600,6 +603,15 @@ export default function MelodyDetail() {
|
|||||||
{downloadName}
|
{downloadName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{missingArchetype && (
|
||||||
|
<span
|
||||||
|
className="text-xs cursor-help"
|
||||||
|
style={{ color: "#f59e0b" }}
|
||||||
|
title="This binary does not exist in the Archetypes"
|
||||||
|
>
|
||||||
|
⚠
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
|
|||||||
@@ -257,7 +257,13 @@ export default function MelodyForm() {
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.open(fileUrl, "_blank", "noopener,noreferrer");
|
const a = document.createElement("a");
|
||||||
|
a.href = fileUrl;
|
||||||
|
a.download = fallbackName || "download.file";
|
||||||
|
a.rel = "noopener noreferrer";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -764,11 +770,21 @@ export default function MelodyForm() {
|
|||||||
<label className="block text-sm font-medium mb-1" style={labelStyle}>Binary File (.bsm)</label>
|
<label className="block text-sm font-medium mb-1" style={labelStyle}>Binary File (.bsm)</label>
|
||||||
{(() => {
|
{(() => {
|
||||||
const { effectiveUrl: binaryUrl, effectiveName: binaryName } = getEffectiveBinary();
|
const { effectiveUrl: binaryUrl, effectiveName: binaryName } = getEffectiveBinary();
|
||||||
|
const missingArchetype = Boolean(pid) && !builtMelody?.id;
|
||||||
return (
|
return (
|
||||||
<div className="text-xs mb-2" style={{ color: "var(--text-muted)" }}>
|
<div className="text-xs mb-2" style={{ color: "var(--text-muted)" }}>
|
||||||
{binaryUrl ? (
|
{binaryUrl ? (
|
||||||
<span className="text-sm mr-4" style={{ color: "var(--text-secondary)" }}>
|
<span className="inline-flex items-center gap-2 text-sm mr-4" style={{ color: "var(--text-secondary)" }}>
|
||||||
{binaryName}
|
<span>{binaryName}</span>
|
||||||
|
{missingArchetype && (
|
||||||
|
<span
|
||||||
|
className="text-xs cursor-help"
|
||||||
|
style={{ color: "#f59e0b" }}
|
||||||
|
title="This binary does not exist in the Archetypes"
|
||||||
|
>
|
||||||
|
⚠
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>No binary uploaded</span>
|
<span>No binary uploaded</span>
|
||||||
|
|||||||
@@ -375,6 +375,7 @@ export default function MelodyList() {
|
|||||||
: `/api/${candidate}`)
|
: `/api/${candidate}`)
|
||||||
: null;
|
: null;
|
||||||
const source = built?.binary_url ? "Archetype" : (url ? "Melody URL" : null);
|
const source = built?.binary_url ? "Archetype" : (url ? "Melody URL" : null);
|
||||||
|
const missingArchetype = Boolean(row?.pid) && !built?.id;
|
||||||
const filename = (() => {
|
const filename = (() => {
|
||||||
if (built?.pid) return `${built.pid}.bsm`;
|
if (built?.pid) return `${built.pid}.bsm`;
|
||||||
if (row?.pid) return `${row.pid}.bsm`;
|
if (row?.pid) return `${row.pid}.bsm`;
|
||||||
@@ -391,7 +392,7 @@ export default function MelodyList() {
|
|||||||
}
|
}
|
||||||
return "melody.bsm";
|
return "melody.bsm";
|
||||||
})();
|
})();
|
||||||
return { url, filename, source, built };
|
return { url, filename, source, built, missingArchetype };
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@@ -466,9 +467,16 @@ export default function MelodyList() {
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const fallbackUrl = resolveEffectiveBinary(row).url;
|
const fallback = resolveEffectiveBinary(row);
|
||||||
|
const fallbackUrl = fallback.url;
|
||||||
if (fallbackUrl) {
|
if (fallbackUrl) {
|
||||||
window.open(fallbackUrl, "_blank", "noopener,noreferrer");
|
const a = document.createElement("a");
|
||||||
|
a.href = fallbackUrl;
|
||||||
|
a.download = fallback.filename || "melody.bsm";
|
||||||
|
a.rel = "noopener noreferrer";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
} else {
|
} else {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
}
|
}
|
||||||
@@ -876,9 +884,13 @@ export default function MelodyList() {
|
|||||||
<span className="inline-flex flex-col">
|
<span className="inline-flex flex-col">
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className="inline-flex items-center gap-2">
|
||||||
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>{filename || "binary.bsm"}</span>
|
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>{filename || "binary.bsm"}</span>
|
||||||
{resolved.source && (
|
{resolved.missingArchetype && (
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ color: "var(--text-muted)", backgroundColor: "var(--bg-card-hover)" }}>
|
<span
|
||||||
{resolved.source}
|
className="text-xs cursor-help"
|
||||||
|
style={{ color: "#f59e0b" }}
|
||||||
|
title="This binary does not exist in the Archetypes"
|
||||||
|
>
|
||||||
|
⚠
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -110,6 +110,14 @@ function applyNoteAssignments(rawStepValue, noteAssignments) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bellDotColor(assignedBell) {
|
||||||
|
const bell = Number(assignedBell || 0);
|
||||||
|
if (bell <= 0) return "rgba(148,163,184,0.7)";
|
||||||
|
const t = Math.min(1, Math.max(0, (bell - 1) / 15));
|
||||||
|
const light = 62 - t * 40; // bright green -> very dark green
|
||||||
|
return `hsl(132, 74%, ${light}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
const mutedStyle = { color: "var(--text-muted)" };
|
const mutedStyle = { color: "var(--text-muted)" };
|
||||||
const labelStyle = { color: "var(--text-secondary)" };
|
const labelStyle = { color: "var(--text-secondary)" };
|
||||||
const NOTE_LABELS = "ABCDEFGHIJKLMNOP";
|
const NOTE_LABELS = "ABCDEFGHIJKLMNOP";
|
||||||
@@ -460,7 +468,9 @@ export default function PlaybackModal({ open, melody, builtMelody, files, archet
|
|||||||
const enabled = Boolean(stepValue & (1 << noteIdx));
|
const enabled = Boolean(stepValue & (1 << noteIdx));
|
||||||
const isCurrent = currentStep === stepIdx;
|
const isCurrent = currentStep === stepIdx;
|
||||||
const assignedBell = Number(noteAssignments[noteIdx] || 0);
|
const assignedBell = Number(noteAssignments[noteIdx] || 0);
|
||||||
const dotLabel = assignedBell > 0 ? assignedBell : noteIdx + 1;
|
const dotLabel = assignedBell > 0 ? assignedBell : "";
|
||||||
|
const isUnassigned = assignedBell <= 0;
|
||||||
|
const dotVisible = enabled || (isUnassigned && Boolean(stepValue & (1 << noteIdx)));
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={`${noteIdx}-${stepIdx}`}
|
key={`${noteIdx}-${stepIdx}`}
|
||||||
@@ -479,15 +489,17 @@ export default function PlaybackModal({ open, melody, builtMelody, files, archet
|
|||||||
width: "68%",
|
width: "68%",
|
||||||
height: "68%",
|
height: "68%",
|
||||||
borderRadius: "9999px",
|
borderRadius: "9999px",
|
||||||
backgroundColor: "var(--btn-primary)",
|
backgroundColor: isUnassigned ? "rgba(100,116,139,0.7)" : bellDotColor(assignedBell),
|
||||||
color: "var(--text-white)",
|
color: "var(--text-white)",
|
||||||
opacity: enabled ? 1 : 0,
|
opacity: dotVisible ? 1 : 0,
|
||||||
transform: enabled ? "scale(1)" : "scale(0.4)",
|
transform: dotVisible ? "scale(1)" : "scale(0.4)",
|
||||||
boxShadow: enabled ? "0 0 10px 3px rgba(116, 184, 22, 0.5)" : "none",
|
boxShadow: dotVisible
|
||||||
|
? (isUnassigned ? "0 0 8px 2px rgba(100,116,139,0.35)" : `0 0 10px 3px ${bellDotColor(assignedBell)}66`)
|
||||||
|
: "none",
|
||||||
transition: "opacity 140ms ease, transform 140ms ease, box-shadow 180ms ease",
|
transition: "opacity 140ms ease, transform 140ms ease, box-shadow 180ms ease",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{enabled ? dotLabel : ""}
|
{dotVisible ? dotLabel : ""}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user