diff --git a/MMM-NextcloudCookbook.css b/MMM-NextcloudCookbook.css
index 1753f55..0c86d3c 100644
--- a/MMM-NextcloudCookbook.css
+++ b/MMM-NextcloudCookbook.css
@@ -15,7 +15,7 @@
inset: 0;
z-index: 20;
overflow: hidden;
- padding: 42px 52px;
+ padding: 50px 58px;
background: #11110f;
}
@@ -40,18 +40,18 @@
}
.MMM-NextcloudCookbook .ncc-recipe h1 {
- margin: 0 100px 18px 0;
+ margin: 0 100px 24px 0;
color: #fff6d8;
- font-size: 52px;
+ font-size: 68px;
line-height: 1.08;
}
.MMM-NextcloudCookbook .ncc-meta {
display: flex;
- gap: 8px 18px;
+ gap: 10px 22px;
align-items: baseline;
- margin: 0 0 24px;
- font-size: 21px;
+ margin: 0 0 32px;
+ font-size: 28px;
}
.MMM-NextcloudCookbook .ncc-meta dt {
@@ -66,25 +66,25 @@
.MMM-NextcloudCookbook .ncc-columns {
display: grid;
grid-template-columns: minmax(280px, 0.8fr) minmax(430px, 1.4fr);
- gap: 46px;
+ gap: 52px;
}
.MMM-NextcloudCookbook .ncc-columns h2 {
- margin: 0 0 12px;
+ margin: 0 0 18px;
color: #d6b96b;
- font-size: 30px;
+ font-size: 42px;
}
.MMM-NextcloudCookbook .ncc-columns ul,
.MMM-NextcloudCookbook .ncc-columns ol {
margin: 0;
padding-left: 1.25em;
- font-size: 23px;
- line-height: 1.3;
+ font-size: 34px;
+ line-height: 1.34;
}
.MMM-NextcloudCookbook .ncc-columns li {
- margin-bottom: 9px;
+ margin-bottom: 14px;
}
.MMM-NextcloudCookbook .ncc-error {
diff --git a/MMM-NextcloudCookbook.js b/MMM-NextcloudCookbook.js
index 0a4a837..01476d9 100644
--- a/MMM-NextcloudCookbook.js
+++ b/MMM-NextcloudCookbook.js
@@ -3,7 +3,9 @@
Module.register("MMM-NextcloudCookbook", {
defaults: {
controlPath: "/MMM-NextcloudCookbook/control",
- animationSpeed: 400
+ animationSpeed: 400,
+ shareUrl: "",
+ sharePassword: ""
},
start() {
diff --git a/README.md b/README.md
index bef1c81..1a1cba4 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,12 @@
# MMM-NextcloudCookbook
A MagicMirror module that displays recipes from a read-only Nextcloud public
-share. It includes a phone-friendly control page for choosing a recipe and
-entering or leaving recipe mode.
+share. It includes a phone-friendly control page for choosing a recipe,
+adjusting its number of portions, and entering or leaving recipe mode.
-The Nextcloud share URL is read only by `node_helper.js` from
-`SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL`. An optional protected-share password can
-be supplied as `SECRET_NEXTCLOUD_COOKBOOK_SHARE_PASSWORD`. Neither value is sent
-to the MagicMirror browser or control page.
+Create a public share for the folder configured as your Nextcloud Cookbook
+recipe directory. Read-only permission is sufficient. Copy the public share
+link; it should look like `https://cloud.example.test/s/SHARE_TOKEN`.
## MagicMirror configuration
@@ -16,11 +15,32 @@ to the MagicMirror browser or control page.
module: "MMM-NextcloudCookbook",
position: "fullscreen_above",
config: {
+ shareUrl: "https://cloud.example.test/s/SHARE_TOKEN",
controlPath: "/MMM-NextcloudCookbook/control"
}
}
```
+For a password-protected public share, add `sharePassword`:
+
+```js
+config: {
+ shareUrl: "https://cloud.example.test/s/SHARE_TOKEN",
+ sharePassword: "PUBLIC_SHARE_PASSWORD"
+}
+```
+
+MagicMirror sends module configuration to its browser clients. A public-share
+URL is normally appropriate there because the token is already a scoped share
+credential, but do not put a Nextcloud account password or app password in this
+configuration.
+
+For deployments that keep even the share token out of browser-visible
+configuration, set `SECRET_NEXTCLOUD_COOKBOOK_SHARE_URL` and, if needed,
+`SECRET_NEXTCLOUD_COOKBOOK_SHARE_PASSWORD` in the MagicMirror process
+environment and omit the corresponding config values. Environment values take
+precedence over module configuration and are read only by `node_helper.js`.
+
Open the controller at:
```text
@@ -31,6 +51,37 @@ The share must contain Nextcloud Cookbook recipe folders with a `recipe.json`
file in each folder. The module uses Nextcloud's token-scoped public DAV API;
protected shares authenticate as Nextcloud's `anonymous` public-share user.
+## Data-source decision: public WebDAV instead of the Cookbook API
+
+Nextcloud Cookbook provides a REST API, but its external API requires
+Nextcloud user credentials on every request. A read-only public-share token
+cannot authenticate to that API.
+
+This module intentionally reads the recipe files through Nextcloud's public
+WebDAV interface because its job is limited to listing and displaying recipes.
+That gives the mirror a narrower security boundary:
+
+- access is scoped to the shared recipe folder;
+- the share can be read-only;
+- the mirror receives no general Nextcloud account credential; and
+- the module cannot create, update, or delete recipes.
+
+Cookbook stores recipes as ordinary `recipe.json` files, so WebDAV provides the
+data required by this read-only display without relying on Cookbook's database
+index. Index synchronization and reindexing matter when an external tool writes
+recipe files; this module does not write them.
+
+The Cookbook API would become preferable if the module later needs
+Cookbook-native search, categories, keywords, imports, or recipe editing. In
+that case, use a dedicated Nextcloud service user with read-only access to the
+recipe folder and a dedicated app password rather than credentials for a
+person's normal account.
+
+References:
+
+- [Cookbook API documentation](https://nextcloud.github.io/cookbook/dev/api/0.1.0/index.html)
+- [Cookbook user documentation](https://nextcloud.github.io/cookbook/user/)
+
## Development
```bash
diff --git a/control/app.js b/control/app.js
index b5368a3..3c3b00c 100644
--- a/control/app.js
+++ b/control/app.js
@@ -3,8 +3,14 @@ const status = document.querySelector("#status");
const list = document.querySelector("#recipes");
const search = document.querySelector("#search");
const exit = document.querySelector("#exit");
+const portionControls = document.querySelector("#portions");
+const portionCount = document.querySelector("#portion-count");
+const fewerPortions = document.querySelector("#fewer-portions");
+const morePortions = document.querySelector("#more-portions");
let recipes = [];
let activeRecipeId = null;
+let activeRecipeName = null;
+let portions = null;
async function request(path, options) {
const response = await fetch(`${apiBase}${path}`, {
@@ -39,6 +45,9 @@ function render() {
return item;
}));
if (!matches.length) status.textContent = "No matching recipes.";
+ portionControls.hidden = !activeRecipeId;
+ portionCount.textContent = portions ?? "";
+ fewerPortions.disabled = portions <= 1;
}
async function activate(recipe) {
@@ -49,6 +58,8 @@ async function activate(recipe) {
body: JSON.stringify({ recipeId: recipe.id })
});
activeRecipeId = state.recipeId;
+ activeRecipeName = recipe.name;
+ portions = state.portions;
status.textContent = `${recipe.name} is now on the mirror.`;
render();
} catch (error) {
@@ -56,11 +67,30 @@ async function activate(recipe) {
}
}
+async function adjustPortions(delta) {
+ try {
+ const state = await request("/portions", {
+ method: "POST",
+ body: JSON.stringify({ delta })
+ });
+ portions = state.portions;
+ status.textContent = `${activeRecipeName} is now on the mirror.`;
+ render();
+ } catch (error) {
+ status.textContent = error.message;
+ }
+}
+
+fewerPortions.addEventListener("click", () => adjustPortions(-1));
+morePortions.addEventListener("click", () => adjustPortions(1));
+
exit.addEventListener("click", async () => {
status.textContent = "Leaving recipe mode…";
try {
await request("/exit", { method: "POST", body: "{}" });
activeRecipeId = null;
+ activeRecipeName = null;
+ portions = null;
status.textContent = "The mirror is back to its normal display.";
render();
} catch (error) {
@@ -73,6 +103,8 @@ search.addEventListener("input", render);
Promise.all([request("/state"), request("/recipes")])
.then(([state, loadedRecipes]) => {
activeRecipeId = state.recipeId;
+ activeRecipeName = state.recipe?.name || null;
+ portions = state.portions;
recipes = loadedRecipes;
status.textContent = `${recipes.length} recipes available.`;
render();
diff --git a/control/index.html b/control/index.html
index 12b7159..86559e6 100644
--- a/control/index.html
+++ b/control/index.html
@@ -16,6 +16,11 @@