Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 318f63657b | |||
| 718157e472 | |||
| f00f11aa55 | |||
| 4081e03970 | |||
| 9cfbf89d46 |
@@ -3,6 +3,7 @@
|
|||||||
## General Rules
|
## General Rules
|
||||||
|
|
||||||
- **English is the primary language**: All code, comments, documentation, commit messages, PR descriptions, and GitHub releases MUST be written in English. The user may communicate in German, but all project artifacts must be in English.
|
- **English is the primary language**: All code, comments, documentation, commit messages, PR descriptions, and GitHub releases MUST be written in English. The user may communicate in German, but all project artifacts must be in English.
|
||||||
|
- **NEVER release without explicit permission**: Do NOT create tags, releases, or version bumps unless the user explicitly asks for it. Always wait for explicit confirmation before any release action.
|
||||||
- **No temporary files**: Delete temporary scripts/files immediately after use. Do not commit temporary debug scripts, test files, or one-off utilities to the repository.
|
- **No temporary files**: Delete temporary scripts/files immediately after use. Do not commit temporary debug scripts, test files, or one-off utilities to the repository.
|
||||||
- **Clean workspace**: Always clean up after yourself. If you create a file for a specific task, delete it once done.
|
- **Clean workspace**: Always clean up after yourself. If you create a file for a specific task, delete it once done.
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,25 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get previous tag
|
||||||
|
id: prev_tag
|
||||||
|
run: |
|
||||||
|
# Get all tags sorted by version, find the one before current
|
||||||
|
CURRENT_TAG=${GITHUB_REF#refs/tags/}
|
||||||
|
PREV_TAG=$(git tag --sort=-v:refname | grep -A1 "^${CURRENT_TAG}$" | tail -1)
|
||||||
|
|
||||||
|
# If no previous tag found (first release), use empty
|
||||||
|
if [ "$PREV_TAG" = "$CURRENT_TAG" ]; then
|
||||||
|
PREV_TAG=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "previous_tag=$PREV_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Current tag: $CURRENT_TAG, Previous tag: $PREV_TAG"
|
||||||
|
|
||||||
- name: Generate changelog
|
- name: Generate changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
run: |
|
run: |
|
||||||
# Get previous tag
|
PREV_TAG="${{ steps.prev_tag.outputs.previous_tag }}"
|
||||||
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
if [ -z "$PREV_TAG" ]; then
|
if [ -z "$PREV_TAG" ]; then
|
||||||
# First release - get all commits
|
# First release - get all commits
|
||||||
@@ -37,6 +51,6 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
body_path: changelog.txt
|
body_path: changelog.txt
|
||||||
generate_release_notes: true
|
generate_release_notes: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
- Email via SMTP
|
- Email via SMTP
|
||||||
- Push notifications via ntfy, Gotify, Telegram, Discord (Shoutrrr)
|
- Push notifications via ntfy, Pushover, Gotify, Telegram, Discord & more ([Shoutrrr](https://containrrr.dev/shoutrrr/))
|
||||||
- Supports both stock warnings and intake reminders
|
- Supports both stock warnings and intake reminders
|
||||||
|
|
||||||
### Privacy & Security
|
### Privacy & Security
|
||||||
@@ -148,6 +148,54 @@ Generate secrets with: `openssl rand -hex 32`
|
|||||||
| `REMINDER_MINUTES_BEFORE` | `15` | Minutes before intake to send reminder |
|
| `REMINDER_MINUTES_BEFORE` | `15` | Minutes before intake to send reminder |
|
||||||
| `EXPIRY_WARNING_DAYS` | `30` | Days before expiry to show warning |
|
| `EXPIRY_WARNING_DAYS` | `30` | Days before expiry to show warning |
|
||||||
|
|
||||||
|
### Push Notifications (Shoutrrr)
|
||||||
|
|
||||||
|
MedAssist uses [Shoutrrr](https://containrrr.dev/shoutrrr/) for push notifications, supporting many services with a single URL format.
|
||||||
|
|
||||||
|
**Supported services:** ntfy, Pushover, Gotify, Discord, Telegram, Slack, Matrix, and [many more](https://containrrr.dev/shoutrrr/v0.8/services/overview/).
|
||||||
|
|
||||||
|
Configure push notifications in Settings → Push, or set defaults via environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DEFAULT_SHOUTRRR_ENABLED` | `false` | Enable push notifications by default |
|
||||||
|
| `DEFAULT_SHOUTRRR_URL` | — | Shoutrrr URL (see examples below) |
|
||||||
|
| `DEFAULT_SHOUTRRR_STOCK_REMINDERS` | `true` | Send stock warnings via push |
|
||||||
|
| `DEFAULT_SHOUTRRR_INTAKE_REMINDERS` | `true` | Send intake reminders via push |
|
||||||
|
|
||||||
|
#### URL Examples
|
||||||
|
|
||||||
|
**ntfy** (free, self-hostable):
|
||||||
|
```
|
||||||
|
ntfy://ntfy.sh/your-topic
|
||||||
|
ntfy://user:password@your-server.com/topic
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pushover** (free app for iOS/Android):
|
||||||
|
```
|
||||||
|
pushover://shoutrrr:API_TOKEN@USER_KEY/
|
||||||
|
```
|
||||||
|
Get your keys at [pushover.net](https://pushover.net/):
|
||||||
|
- **User Key**: Shown on your dashboard (top right)
|
||||||
|
- **API Token**: Create an application → copy the API Token
|
||||||
|
|
||||||
|
**Gotify** (self-hosted):
|
||||||
|
```
|
||||||
|
gotify://your-server.com/TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
**Discord**:
|
||||||
|
```
|
||||||
|
discord://TOKEN@WEBHOOK_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
**Telegram**:
|
||||||
|
```
|
||||||
|
telegram://TOKEN@telegram?chats=CHAT_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
For all services and options, see the [Shoutrrr documentation](https://containrrr.dev/shoutrrr/v0.8/services/overview/).
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "medassist-ng-frontend",
|
"name": "medassist-ng-frontend",
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "medassist-ng-frontend",
|
"name": "medassist-ng-frontend",
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
|
|||||||
+20
-42
@@ -354,7 +354,7 @@ function AppContent() {
|
|||||||
// Export/Import state
|
// Export/Import state
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [includeSensitiveData, setIncludeSensitiveData] = useState(false);
|
|
||||||
const [showImportConfirm, setShowImportConfirm] = useState(false);
|
const [showImportConfirm, setShowImportConfirm] = useState(false);
|
||||||
const [pendingImportData, setPendingImportData] = useState<any>(null);
|
const [pendingImportData, setPendingImportData] = useState<any>(null);
|
||||||
// Collapsed days state (manually collapsed days are persisted)
|
// Collapsed days state (manually collapsed days are persisted)
|
||||||
@@ -788,7 +788,7 @@ function AppContent() {
|
|||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
setExporting(true);
|
setExporting(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/export?includeSensitive=${includeSensitiveData}`, {
|
const res = await fetch('/api/export?includeSensitive=true', {
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error("Export failed");
|
if (!res.ok) throw new Error("Export failed");
|
||||||
@@ -2243,12 +2243,12 @@ function AppContent() {
|
|||||||
<span className="field-label">{t('settings.push.url')}</span>
|
<span className="field-label">{t('settings.push.url')}</span>
|
||||||
<div className="input-with-tooltip">
|
<div className="input-with-tooltip">
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="text"
|
||||||
value={settings.shoutrrrUrl}
|
value={settings.shoutrrrUrl}
|
||||||
onChange={(e) => setSettings({ ...settings, shoutrrrUrl: e.target.value })}
|
onChange={(e) => setSettings({ ...settings, shoutrrrUrl: e.target.value })}
|
||||||
placeholder="https://ntfy.sh/your-topic"
|
placeholder={t('settings.push.urlPlaceholder')}
|
||||||
/>
|
/>
|
||||||
<span className="info-tooltip" data-tooltip={t('settings.push.supports')}>ⓘ</span>
|
<span className="info-tooltip" data-tooltip={`${t('settings.push.supports')}\n\n${t('settings.push.docsLink')}`}>ⓘ</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -2412,60 +2412,38 @@ function AppContent() {
|
|||||||
{/* Export/Import Section */}
|
{/* Export/Import Section */}
|
||||||
<article className="card">
|
<article className="card">
|
||||||
<div className="card-head">
|
<div className="card-head">
|
||||||
<h2>{t('exportImport.title')}</h2>
|
<h2>
|
||||||
|
{t('exportImport.title')}
|
||||||
|
<span className="info-tooltip" data-tooltip={t('exportImport.description')}>ⓘ</span>
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="setting-section">
|
<div className="setting-section">
|
||||||
<p className="hint-text" style={{marginBottom: "16px"}}>{t('exportImport.description')}</p>
|
<div className="export-import-grid">
|
||||||
|
{/* Export */}
|
||||||
{/* Export */}
|
<div className="export-import-card">
|
||||||
<div className="setting-group" style={{marginBottom: "24px"}}>
|
<h3>{t('exportImport.exportTitle')}</h3>
|
||||||
<div className="export-controls">
|
<p className="export-import-desc">{t('exportImport.exportDesc')}</p>
|
||||||
<label className="checkbox-label" style={{marginBottom: "12px", display: "flex", alignItems: "center", gap: "8px"}}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={includeSensitiveData}
|
|
||||||
onChange={(e) => setIncludeSensitiveData(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>{t('exportImport.includeSensitive')}</span>
|
|
||||||
</label>
|
|
||||||
{includeSensitiveData && (
|
|
||||||
<p className="hint-text warning-text" style={{marginBottom: "12px", color: "var(--warning)", fontSize: "0.85rem"}}>
|
|
||||||
⚠️ {t('exportImport.sensitiveWarning')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary"
|
className="secondary"
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
disabled={exporting}
|
disabled={exporting}
|
||||||
style={{marginRight: "12px"}}
|
|
||||||
>
|
>
|
||||||
{exporting ? t('exportImport.exporting') : t('exportImport.export')}
|
{exporting ? t('exportImport.exporting') : t('exportImport.export')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Import */}
|
||||||
{/* Import */}
|
<div className="export-import-card">
|
||||||
<div className="setting-group">
|
<h3>{t('exportImport.importTitle')}</h3>
|
||||||
<div className="import-controls">
|
<p className="export-import-desc">{t('exportImport.importDesc')}</p>
|
||||||
<label className="secondary" style={{
|
<label className="export-import-file-btn">
|
||||||
cursor: "pointer",
|
|
||||||
display: "inline-block",
|
|
||||||
padding: "0.7rem 1.25rem",
|
|
||||||
borderRadius: "var(--btn-radius)",
|
|
||||||
background: "var(--bg-tertiary)",
|
|
||||||
color: "var(--text-primary)",
|
|
||||||
border: "1px solid var(--border-secondary)",
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: "0.9rem"
|
|
||||||
}}>
|
|
||||||
{importing ? t('exportImport.importing') : t('exportImport.import')}
|
{importing ? t('exportImport.importing') : t('exportImport.import')}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".json,application/json"
|
accept=".json,application/json"
|
||||||
onChange={handleImportFileSelect}
|
onChange={handleImportFileSelect}
|
||||||
disabled={importing}
|
disabled={importing}
|
||||||
style={{display: "none"}}
|
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -173,7 +173,9 @@
|
|||||||
},
|
},
|
||||||
"push": {
|
"push": {
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"supports": "Unterstützt ntfy, Discord, Telegram, Slack"
|
"urlPlaceholder": "ntfy://topic oder pushover://:token@userkey/",
|
||||||
|
"supports": "Unterstützt ntfy, Pushover, Gotify, Discord, Telegram, Slack & mehr",
|
||||||
|
"docsLink": "Siehe shoutrrr.dev für alle Services"
|
||||||
},
|
},
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"title": "Erinnerungsplan",
|
"title": "Erinnerungsplan",
|
||||||
@@ -351,14 +353,18 @@
|
|||||||
},
|
},
|
||||||
"exportImport": {
|
"exportImport": {
|
||||||
"title": "Daten Export / Import",
|
"title": "Daten Export / Import",
|
||||||
"description": "Exportiere deine Daten zur Sicherung oder Übertragung auf ein anderes Gerät. Import ersetzt ALLE deine bestehenden Daten.",
|
"description": "Sichere deine Daten oder übertrage sie auf ein anderes Gerät.",
|
||||||
|
"exportTitle": "Export",
|
||||||
|
"exportDesc": "Lade alle deine Daten als JSON-Datei herunter.",
|
||||||
|
"importTitle": "Import",
|
||||||
|
"importDesc": "Stelle Daten aus einer Sicherung wieder her. Dies ersetzt alle bestehenden Daten.",
|
||||||
"export": "Daten exportieren",
|
"export": "Daten exportieren",
|
||||||
"exporting": "Exportiere...",
|
"exporting": "Exportiere...",
|
||||||
"import": "Daten importieren",
|
"import": "Datei auswählen",
|
||||||
"importing": "Importiere...",
|
"importing": "Importiere...",
|
||||||
"selectFile": "Datei auswählen",
|
"selectFile": "Datei auswählen",
|
||||||
"includeSensitive": "Sensible Daten einschließen",
|
"includeSensitive": "Sensible Daten einschließen (Benachrichtigungs-URLs)",
|
||||||
"sensitiveWarning": "Warnung: Dies fügt Benachrichtigungs-URLs (können Passwörter enthalten) im Klartext in die Exportdatei ein.",
|
"sensitiveWarning": "Benachrichtigungs-URLs können Passwörter enthalten und werden im Klartext gespeichert.",
|
||||||
"confirmImport": "Alle Daten ersetzen?",
|
"confirmImport": "Alle Daten ersetzen?",
|
||||||
"confirmImportMessage": "Dies löscht dauerhaft alle deine aktuellen Medikamente, Einnahmehistorie, Einstellungen und Teilen-Links und ersetzt sie durch die importierten Daten.",
|
"confirmImportMessage": "Dies löscht dauerhaft alle deine aktuellen Medikamente, Einnahmehistorie, Einstellungen und Teilen-Links und ersetzt sie durch die importierten Daten.",
|
||||||
"confirmImportWarning": "Diese Aktion kann nicht rückgängig gemacht werden!",
|
"confirmImportWarning": "Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||||
|
|||||||
@@ -175,7 +175,9 @@
|
|||||||
},
|
},
|
||||||
"push": {
|
"push": {
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"supports": "Supports ntfy, Discord, Telegram, Slack"
|
"urlPlaceholder": "ntfy://topic or pushover://:token@userkey/",
|
||||||
|
"supports": "Supports ntfy, Pushover, Gotify, Discord, Telegram, Slack & more",
|
||||||
|
"docsLink": "See shoutrrr.dev for all services"
|
||||||
},
|
},
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"title": "Reminder Schedule",
|
"title": "Reminder Schedule",
|
||||||
@@ -353,14 +355,18 @@
|
|||||||
},
|
},
|
||||||
"exportImport": {
|
"exportImport": {
|
||||||
"title": "Data Export / Import",
|
"title": "Data Export / Import",
|
||||||
"description": "Export your data for backup or transfer to another device. Import will replace ALL your existing data.",
|
"description": "Backup your data or transfer it to another device.",
|
||||||
|
"exportTitle": "Export",
|
||||||
|
"exportDesc": "Download all your data as a JSON file.",
|
||||||
|
"importTitle": "Import",
|
||||||
|
"importDesc": "Restore data from a backup file. This will replace all existing data.",
|
||||||
"export": "Export Data",
|
"export": "Export Data",
|
||||||
"exporting": "Exporting...",
|
"exporting": "Exporting...",
|
||||||
"import": "Import Data",
|
"import": "Select File",
|
||||||
"importing": "Importing...",
|
"importing": "Importing...",
|
||||||
"selectFile": "Select File",
|
"selectFile": "Select File",
|
||||||
"includeSensitive": "Include sensitive data",
|
"includeSensitive": "Include sensitive data (notification URLs)",
|
||||||
"sensitiveWarning": "Warning: This will include notification URLs (may contain passwords) in plain text in the export file.",
|
"sensitiveWarning": "Notification URLs may contain passwords and will be stored in plain text.",
|
||||||
"confirmImport": "Replace All Data?",
|
"confirmImport": "Replace All Data?",
|
||||||
"confirmImportMessage": "This will permanently delete all your current medications, dose history, settings, and share links, then replace them with the imported data.",
|
"confirmImportMessage": "This will permanently delete all your current medications, dose history, settings, and share links, then replace them with the imported data.",
|
||||||
"confirmImportWarning": "This action cannot be undone!",
|
"confirmImportWarning": "This action cannot be undone!",
|
||||||
|
|||||||
@@ -3977,3 +3977,79 @@ h3 .reminder-icon.info-tooltip {
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Export/Import Section */
|
||||||
|
.card:has(.export-import-grid) {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:has(.export-import-grid) .card-head {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.export-import-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-card {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-secondary);
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-card h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-desc {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-card button,
|
||||||
|
.export-import-file-btn {
|
||||||
|
margin-top: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-file-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.7rem 1.25rem;
|
||||||
|
border-radius: var(--btn-radius);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-file-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-import-file-btn input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user