feat: synchronize recipe completion controls

Fixes #5\nRefs #1, #2
This commit is contained in:
2026-08-14 13:28:57 -05:00
parent b61ca70bed
commit 6f28f83f4a
10 changed files with 334 additions and 29 deletions
+54 -7
View File
@@ -5,11 +5,15 @@ const NodeHelper = require("node_helper");
const QRCode = require("qrcode");
const NextcloudPublicShare = require("./lib/nextcloud-public-share");
const { parseYield, scaleRecipe } = require("./lib/recipe-scaler");
const { normalizeProgress, updateProgress } = require("./lib/recipe-progress");
module.exports = NodeHelper.create({
start() {
this.config = { controlPath: "/MMM-NextcloudCookbook/control" };
this.state = { active: false, recipeId: null, recipe: null, portions: null, error: null };
this.state = {
active: false, recipeId: null, recipe: null, portions: null,
progress: normalizeProgress(), error: null
};
this.recipeCache = { expires: 0, recipes: [] };
this.stateFile = path.join(process.cwd(), "config", "MMM-NextcloudCookbook-state.json");
this.configureClient();
@@ -18,6 +22,10 @@ module.exports = NodeHelper.create({
},
socketNotificationReceived(notification, payload) {
if (notification === "UPDATE_PROGRESS") {
this.updateProgressFromMirror(payload);
return;
}
if (notification !== "CONFIG") return;
this.config = { ...this.config, ...payload };
try {
@@ -82,11 +90,17 @@ module.exports = NodeHelper.create({
return response.status(400).json({ error: "recipeId is required" });
}
const recipe = await this.getClient().getRecipe(recipeId);
const isResumingRecipe = this.state.recipeId === recipeId
&& Number.isInteger(this.state.portions)
&& this.state.portions > 0;
this.state = {
active: true,
recipeId,
recipe,
portions: Math.max(1, Math.round(parseYield(recipe.recipeYield) || 1)),
portions: isResumingRecipe
? this.state.portions
: Math.max(1, Math.round(parseYield(recipe.recipeYield) || 1)),
progress: isResumingRecipe ? this.state.progress : normalizeProgress(),
error: null
};
await this.saveState();
@@ -113,9 +127,25 @@ module.exports = NodeHelper.create({
return next(error);
}
});
this.expressApp.post("/MMM-NextcloudCookbook/api/progress", async (request, response, next) => {
try {
if (!this.state.active || !this.state.recipe) {
return response.status(409).json({ error: "No recipe is active" });
}
this.state.progress = updateProgress(this.state.recipe, this.state.progress, request.body || {});
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
return response.json(this.publicState());
} catch (error) {
if (error.message === "Invalid progress update" || error.message === "Progress item does not exist") {
return response.status(400).json({ error: error.message });
}
return next(error);
}
});
this.expressApp.post("/MMM-NextcloudCookbook/api/exit", async (_request, response, next) => {
try {
this.state = { active: false, recipeId: null, recipe: null, portions: null, error: null };
this.state = { ...this.state, active: false, error: null };
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
response.json(this.publicState());
@@ -144,12 +174,13 @@ module.exports = NodeHelper.create({
async loadState() {
try {
const persisted = JSON.parse(await fs.readFile(this.stateFile, "utf8"));
if (!persisted.active || !persisted.recipeId) return;
if (!persisted.recipeId) return;
this.state = {
active: true,
active: Boolean(persisted.active),
recipeId: persisted.recipeId,
recipe: null,
portions: Number.isInteger(persisted.portions) ? persisted.portions : null,
progress: normalizeProgress(persisted.progress),
error: null
};
} catch (error) {
@@ -174,7 +205,8 @@ module.exports = NodeHelper.create({
await fs.writeFile(temporary, `${JSON.stringify({
active: this.state.active,
recipeId: this.state.recipeId,
portions: this.state.portions
portions: this.state.portions,
progress: this.state.progress
})}\n`, {
mode: 0o600
});
@@ -190,9 +222,24 @@ module.exports = NodeHelper.create({
return {
active: this.state.active,
recipeId: this.state.recipeId,
recipe: this.state.recipe ? scaleRecipe(this.state.recipe, this.state.portions) : null,
recipe: this.state.active && this.state.recipe
? scaleRecipe(this.state.recipe, this.state.portions)
: null,
portions: this.state.portions,
progress: this.state.progress,
error: this.state.error
};
},
async updateProgressFromMirror(payload) {
try {
if (!this.state.active || !this.state.recipe) return;
this.state.progress = updateProgress(this.state.recipe, this.state.progress, payload || {});
await this.saveState();
this.sendSocketNotification("STATE", this.publicState());
} catch (error) {
console.error(`[MMM-NextcloudCookbook] ${error.message}`);
this.sendSocketNotification("STATE", this.publicState());
}
}
});