Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/advanced.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Advanced Topics

!!! info "First Of All"
!!! info "Optional Features"
Take note that none of this section is required to run the base program.
These are just some fun additions, which you can optionally use.
If you are not interested in them, just skip this section.
Expand Down
2 changes: 1 addition & 1 deletion docs/payment.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Setting Up and Using the Payment Feature

!!! info "First Of All"
!!! info "Optional Feature"
Take note that none of this section is required to run the base program.
This is a way to run your CocktailBerry machines in a more commercial mode.
If you are not interested in them, just skip this section.
Expand Down
17 changes: 10 additions & 7 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,16 @@ They can be used at own risk of CocktailBerry not working 100% properly.
??? info "List Software Config Values"
Software config values are used to configure additional connected software and its behavior.

| Value Name | Description |
| :---------------------- | :--------------------------------------------------------- |
| `MICROSERVICE_ACTIVE` | Post to microservice set up by docker |
| `MICROSERVICE_BASE_URL` | Base URL for microservice (default: http://127.0.0.1:5000) |
| `TEAMS_ACTIVE` | Use teams feature |
| `TEAM_BUTTON_NAMES` | List of format ["Team1", "Team2"] |
| `TEAM_API_URL` | Endpoint of teams API, default used port by API is 8080 |
| Value Name | Description |
| :----------------------------- | :--------------------------------------------------------- |
| `MICROSERVICE_ACTIVE` | Post to microservice set up by docker |
| `MICROSERVICE_BASE_URL` | Base URL for microservice (default: http://127.0.0.1:5000) |
| `TEAMS_ACTIVE` | Use teams feature |
| `TEAM_BUTTON_NAMES` | List of format ["Team1", "Team2"] |
| `TEAM_API_URL` | Endpoint of teams API, default used port by API is 8080 |
| `WAITER_MODE` | Enable or disable Service Personnel Mode |
| `WAITER_LOGOUT_AFTER_COCKTAIL` | Log out after cocktail preparation |
| `WAITER_AUTO_LOGOUT_S` | Log out after x seconds of inactivity (0 = disabled) |

??? info "Payment Related Config Values"
Payment config values are used to configure the payment system and its behavior.
Expand Down
100 changes: 100 additions & 0 deletions docs/waiter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Service Personnel Mode

!!! info "Optional Feature"
Service Personnel Mode is an optional feature for operators who want to track which staff member prepared each cocktail and control access to the machine.
If you don't need staff tracking or per-person access control, you can skip this section.

!!! warning "NFC Reader Conflict"
Service Personnel Mode and CocktailBerry NFC Payment both require the NFC reader.
They cannot be enabled at the same time, nor is the initial intend that they would be used together.
If you need payment functionality, see the [Payment Feature](payment.md) instead.

## Overview

Service Personnel Mode lets you manage who is using your CocktailBerry machine.
Each staff member gets an NFC chip that they scan to log in before they can prepare cocktails.
This gives you two main benefits:

- **Accountability**: Every cocktail is logged with the name of the person who made it, giving you full visibility over your operation.
- **Access Control**: Authorized staff can bypass the maker password for different specified tabs. So you do not need to share the password, but still control who can access what.

## Enabling Service Personnel Mode

To enable the feature, activate `WAITER_MODE` in the configuration, you can find it under the software section.
You will need an NFC reader connected to your machine (same hardware as for the [payment](payment.md) feature).
On startup, CocktailBerry validates that the NFC reader is available and disables the mode gracefully if it is not.

??? info "Configuration Options"

| Setting | Description |
| ------------------------------ | ---------------------------------------------------- |
| `WAITER_MODE` | Enable or disable Service Personnel Mode |
| `WAITER_LOGOUT_AFTER_COCKTAIL` | Log out after cocktail preparation |
| `WAITER_AUTO_LOGOUT_S` | Log out after x seconds of inactivity (0 = disabled) |

## Registering Staff

Before your staff can use the machine, you need to register their NFC chips.
Open the Service Personnel management window from the options menu.
When a staff member scans their NFC chip, the scanned ID appears in the management view.
You can then assign a name and select which permissions that person should have (Maker, Ingredients, Recipes, Bottles).
Each permission controls whether the person can access that tab, and whether they can bypass the maker password for it.

## Login and Logout Flow

The typical workflow looks like this:

```mermaid
sequenceDiagram
actor s as Staff Member
participant nfc as NFC Reader
participant cb as CocktailBerry
participant db as Database
s->>nfc: Scans NFC Chip
nfc->>cb: Reads Chip ID
cb->>db: Looks Up Staff Member
db->>cb: Returns Name & Permissions
cb->>cb: Staff Member Logged In
s->>cb: Prepares Cocktail
cb->>db: Logs Cocktail with Staff Info
cb->>cb: Auto-Logout (if configured)
```

Once logged in, the staff member can then prepare cocktails, and each preparation is logged to their name.
Logout happens either manually, automatically after a cocktail, or after a configured timeout.

### Difference in Appearance Between v1 and v2

Since we use distinct GUI technologies for v1 (Qt) and v2 (Web), there are some differences in how the logged-in staff member is displayed and how logout works.
See the according section for your version below.

=== "v1"

On the maker view, there is no dedicated staff indicator or logout button.
Logout happens through the configured auto-logout settings or by scanning a different chip.

=== "v2"

The currently logged-in staff member is displayed as an inline badge on the maker screen, showing their name.
Clicking the badge reveals a dedicated logout button, allowing the staff member to log out directly.

### Password Bypass

If a staff member has the appropriate permissions, they can bypass the maker password for the tabs they have access to.
See some specifics on implementation and differences between v1 and v2 below.

=== "v1"

Staff members need to be logged in (e.g. by scanning their NFC chip) before they can access a protected tab without entering the maker password.
Scanning the NFC chip while already be prompted for the password will not remove the password, so be sure to log in before navigating to the protected tab.

=== "v2"

Staff members can also scan the NFC when prompted for the maker password, which will bypass the password if they have the required permissions.

## Statistics

Every cocktail prepared while a staff member is logged in is recorded with their name, the recipe, volume, and timestamp.
You can view these logs in the Statistics tab of the Service Personnel management window.
Logs are grouped by date and staff member, showing the total number of cocktails and volume per person per day.
This helps you track performance and accountability across your team.
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]

# Global variables
qt_ui_files := "available bonusingredient bottlewindow cocktailmanager calibration calibration_real calibration_target customdialog datepicker keyboard optionwindow numpad progressbarwindow teamselection passworddialog customprompt logwindow rfidwriter wifiwindow customcolor addonwindow addonmanager datawindow cocktail_selection picture_window refill_prompt config_window resource_window news_window sumup_reader_window event_window"
qt_ui_files := "available bonusingredient bottlewindow cocktailmanager calibration calibration_real calibration_target customdialog datepicker keyboard optionwindow numpad progressbarwindow teamselection passworddialog customprompt logwindow rfidwriter wifiwindow customcolor addonwindow addonmanager datawindow cocktail_selection picture_window refill_prompt config_window resource_window news_window sumup_reader_window event_window waiter_window"

# Install all Python dependencies (main + dev)
[group('Python Environment & Dependencies')]
Expand Down
20 changes: 18 additions & 2 deletions src/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from src.api.internal.log_config import log_config
from src.api.internal.validation import ValidationError
from src.api.models import AboutInfo, ApiMessage
from src.api.routers import bottles, cocktails, ingredients, options
from src.api.routers import bottles, cocktails, ingredients, options, waiters
from src.config.config_manager import CONFIG as cfg
from src.config.config_manager import shared
from src.config.errors import ConfigError
Expand All @@ -26,7 +26,14 @@
from src.programs.addons import ADDONS, CouldNotInstallAddonError
from src.resource_stats import start_resource_tracker
from src.service.nfc_payment_service import NFCPaymentService
from src.startup_checks import can_update, check_payment_service, connection_okay, is_python_deprecated
from src.service.waiter_service import WaiterService
from src.startup_checks import (
can_update,
check_payment_service,
check_waiter_mode,
connection_okay,
is_python_deprecated,
)
from src.updater import UpdateInfo, Updater
from src.utils import get_platform_data

Expand All @@ -53,6 +60,11 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
_logger.warning(f"Payment service check failed: {payment_check.reason}. Disabling payment.")
cfg.PAYMENT_TYPE = "Disabled"
shared.startup_payment_issue.set_issue(message=payment_check.reason)
waiter_check = check_waiter_mode()
if not waiter_check.ok:
_logger.warning(f"Waiter mode check failed: {waiter_check.reason}. Disabling waiter mode.")
cfg.WAITER_MODE = False
shared.startup_waiter_issue.set_issue(message=waiter_check.reason)
mc = MachineController()
mc.init_machine()
ADDONS.setup_addons()
Expand All @@ -70,6 +82,8 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
ADDONS.start_trigger_loop()
if cfg.cocktailberry_payment:
NFCPaymentService().start_continuous_sensing()
if cfg.waiter_mode_active:
WaiterService().start_continuous_sensing()
yield
mc.cleanup()

Expand Down Expand Up @@ -139,6 +153,8 @@ async def validation_error_handler(request: Request, exc: ValidationError) -> JS
app.include_router(options.protected_router)
app.include_router(ingredients.router)
app.include_router(ingredients.protected_router)
app.include_router(waiters.router)
app.include_router(waiters.protected_router)


@app.get("/", tags=[Tags.TESTING], summary="Test endpoint, check if api works")
Expand Down
5 changes: 5 additions & 0 deletions src/api/api_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Tags(StrEnum):
INGREDIENTS = "ingredients"
OPTIONS = "options"
PAYMENT = "payment"
WAITERS = "waiters"
MAKER_PROTECTED = "maker protected"
MASTER_PROTECTED = "master protected"
TESTING = "testing"
Expand Down Expand Up @@ -75,6 +76,10 @@ class Tags(StrEnum):
"name": Tags.PAYMENT,
"description": "Operation related to the payment service.",
},
{
"name": Tags.WAITERS,
"description": "Service Personnel mode management and tracking.",
},
{
"name": Tags.MAKER_PROTECTED,
"description": "Need x-maker-key header with the maker password, if this section is set to protected and password is set.", # noqa: E501
Expand Down
18 changes: 17 additions & 1 deletion src/api/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi.security import APIKeyHeader

from src.config.config_manager import CONFIG as cfg
from src.config.config_manager import Tab
from src.config.config_manager import Tab, shared

master_password_header = APIKeyHeader(name="x-master-key", scheme_name="Master Password", auto_error=False)
maker_password_header = APIKeyHeader(name="x-maker-key", scheme_name="Maker Password", auto_error=False)
Expand All @@ -19,6 +19,20 @@ def _parse_password(password: str | None) -> int:
raise HTTPException(status_code=403, detail="Invalid Password")


def _waiter_has_tab_permission(tab: Tab) -> bool:
waiter = shared.current_waiter
if waiter is None:
return False

permission_by_tab = {
Tab.MAKER: waiter.permissions.maker,
Tab.INGREDIENTS: waiter.permissions.ingredients,
Tab.RECIPES: waiter.permissions.recipes,
Tab.BOTTLES: waiter.permissions.bottles,
}
return permission_by_tab.get(tab, False)


def master_protected_dependency(master_password: str | None = Security(master_password_header)) -> None:
if cfg.UI_MASTERPASSWORD == 0:
return
Expand All @@ -33,6 +47,8 @@ def dependency(maker_password: str | None = Security(maker_password_header)) ->
return
if not cfg.UI_LOCKED_TABS[tab]:
return
if cfg.waiter_mode_active and _waiter_has_tab_permission(tab):
return
password = _parse_password(maker_password)
if password != cfg.UI_MAKER_PASSWORD:
raise HTTPException(status_code=403, detail="Invalid Maker Password")
Expand Down
63 changes: 60 additions & 3 deletions src/api/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from typing import Annotated, TypeVar
from __future__ import annotations

from typing import TYPE_CHECKING, Annotated, TypeVar

from annotated_types import Len
from pydantic import BaseModel, Field

from src.config.config_manager import StartupIssue
from src.models import Event, PrepareResult

if TYPE_CHECKING:
from src.db_models import DbWaiter

T = TypeVar("T")


Expand All @@ -29,8 +34,8 @@ class CocktailIngredient(BaseModel):


class CocktailsAndIngredients(BaseModel):
cocktails: list["Cocktail"]
ingredients: list["Ingredient"]
cocktails: list[Cocktail]
ingredients: list[Ingredient]


class Cocktail(BaseModel):
Expand Down Expand Up @@ -117,6 +122,7 @@ class IssueData(BaseModel):
internet: StartupIssue
config: StartupIssue
payment: StartupIssue
waiter: StartupIssue


class DateTimeInput(BaseModel):
Expand Down Expand Up @@ -148,3 +154,54 @@ class SumupReaderResponse(BaseModel):
class SumupReaderCreate(BaseModel):
name: str
pairing_code: str


class WaiterPermissions(BaseModel):
maker: bool = False
ingredients: bool = False
recipes: bool = False
bottles: bool = False


class WaiterResponse(BaseModel):
nfc_id: str
name: str
permissions: WaiterPermissions

@classmethod
def from_db(cls, waiter: DbWaiter) -> WaiterResponse:
return cls(
nfc_id=waiter.nfc_id,
name=waiter.name,
permissions=WaiterPermissions(
maker=waiter.privilege_maker,
ingredients=waiter.privilege_ingredients,
recipes=waiter.privilege_recipes,
bottles=waiter.privilege_bottles,
),
)


class WaiterCreate(BaseModel):
nfc_id: str
name: str
permissions: WaiterPermissions | None = None


class WaiterUpdate(BaseModel):
name: str | None = None
permissions: WaiterPermissions | None = None


class WaiterLogEntry(BaseModel):
id: int
timestamp: str
waiter_name: str
recipe_name: str
volume: int
is_virgin: bool


class CurrentWaiterState(BaseModel):
nfc_id: str | None = None
waiter: WaiterResponse | None = None
2 changes: 2 additions & 0 deletions src/api/routers/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ async def check_issues() -> IssueData:
internet=shared.startup_need_time_adjustment,
config=shared.startup_config_issue,
payment=shared.startup_payment_issue,
waiter=shared.startup_waiter_issue,
)


Expand All @@ -373,6 +374,7 @@ async def ignore_issues() -> ApiMessage:
shared.startup_need_time_adjustment.set_ignored()
shared.startup_config_issue.set_ignored()
shared.startup_payment_issue.set_ignored()
shared.startup_waiter_issue.set_ignored()
return ApiMessage(message="Issues ignored")


Expand Down
Loading