Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 318f63657b | |||
| 718157e472 | |||
| f00f11aa55 | |||
| 4081e03970 | |||
| 9cfbf89d46 |
@@ -3,6 +3,7 @@
|
||||
## 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.
|
||||
- **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.
|
||||
- **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:
|
||||
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
|
||||
id: changelog
|
||||
run: |
|
||||
# Get previous tag
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||
PREV_TAG="${{ steps.prev_tag.outputs.previous_tag }}"
|
||||
|
||||
if [ -z "$PREV_TAG" ]; then
|
||||
# First release - get all commits
|
||||
@@ -37,6 +51,6 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: changelog.txt
|
||||
generate_release_notes: true
|
||||
generate_release_notes: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
### Notifications
|
||||
- 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
|
||||
|
||||
### Privacy & Security
|
||||
@@ -148,6 +148,54 @@ Generate secrets with: `openssl rand -hex 32`
|
||||
| `REMINDER_MINUTES_BEFORE` | `15` | Minutes before intake to send reminder |
|
||||
| `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
|
||||
|
||||
```bash
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "medassist-ng-frontend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "medassist-ng-frontend",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.0",
|
||||
"dependencies": {
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-browser-languagedetector": "^8.0.4",
|
||||
|
||||
+20
-42
@@ -354,7 +354,7 @@ function AppContent() {
|
||||
// Export/Import state
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [includeSensitiveData, setIncludeSensitiveData] = useState(false);
|
||||
|
||||
const [showImportConfirm, setShowImportConfirm] = useState(false);
|
||||
const [pendingImportData, setPendingImportData] = useState<any>(null);
|
||||
// Collapsed days state (manually collapsed days are persisted)
|
||||
@@ -788,7 +788,7 @@ function AppContent() {
|
||||
async function handleExport() {
|
||||
setExporting(true);
|
||||
try {
|
||||
const res = await fetch(`/api/export?includeSensitive=${includeSensitiveData}`, {
|
||||
const res = await fetch('/api/export?includeSensitive=true', {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error("Export failed");
|
||||
@@ -2243,12 +2243,12 @@ function AppContent() {
|
||||
<span className="field-label">{t('settings.push.url')}</span>
|
||||
<div className="input-with-tooltip">
|
||||
<input
|
||||
type="url"
|
||||
type="text"
|
||||
value={settings.shoutrrrUrl}
|
||||
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>
|
||||
</label>
|
||||
</div>
|
||||
@@ -2412,60 +2412,38 @@ function AppContent() {
|
||||
{/* Export/Import Section */}
|
||||
<article className="card">
|
||||
<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 className="setting-section">
|
||||
<p className="hint-text" style={{marginBottom: "16px"}}>{t('exportImport.description')}</p>
|
||||
|
||||
{/* Export */}
|
||||
<div className="setting-group" style={{marginBottom: "24px"}}>
|
||||
<div className="export-controls">
|
||||
<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>
|
||||
)}
|
||||
<div className="export-import-grid">
|
||||
{/* Export */}
|
||||
<div className="export-import-card">
|
||||
<h3>{t('exportImport.exportTitle')}</h3>
|
||||
<p className="export-import-desc">{t('exportImport.exportDesc')}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={handleExport}
|
||||
disabled={exporting}
|
||||
style={{marginRight: "12px"}}
|
||||
>
|
||||
{exporting ? t('exportImport.exporting') : t('exportImport.export')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import */}
|
||||
<div className="setting-group">
|
||||
<div className="import-controls">
|
||||
<label className="secondary" style={{
|
||||
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"
|
||||
}}>
|
||||
|
||||
{/* Import */}
|
||||
<div className="export-import-card">
|
||||
<h3>{t('exportImport.importTitle')}</h3>
|
||||
<p className="export-import-desc">{t('exportImport.importDesc')}</p>
|
||||
<label className="export-import-file-btn">
|
||||
{importing ? t('exportImport.importing') : t('exportImport.import')}
|
||||
<input
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
onChange={handleImportFileSelect}
|
||||
disabled={importing}
|
||||
style={{display: "none"}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -173,7 +173,9 @@
|
||||
},
|
||||
"push": {
|
||||
"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": {
|
||||
"title": "Erinnerungsplan",
|
||||
@@ -351,14 +353,18 @@
|
||||
},
|
||||
"exportImport": {
|
||||
"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",
|
||||
"exporting": "Exportiere...",
|
||||
"import": "Daten importieren",
|
||||
"import": "Datei auswählen",
|
||||
"importing": "Importiere...",
|
||||
"selectFile": "Datei auswählen",
|
||||
"includeSensitive": "Sensible Daten einschließen",
|
||||
"sensitiveWarning": "Warnung: Dies fügt Benachrichtigungs-URLs (können Passwörter enthalten) im Klartext in die Exportdatei ein.",
|
||||
"includeSensitive": "Sensible Daten einschließen (Benachrichtigungs-URLs)",
|
||||
"sensitiveWarning": "Benachrichtigungs-URLs können Passwörter enthalten und werden im Klartext gespeichert.",
|
||||
"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.",
|
||||
"confirmImportWarning": "Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||
|
||||
@@ -175,7 +175,9 @@
|
||||
},
|
||||
"push": {
|
||||
"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": {
|
||||
"title": "Reminder Schedule",
|
||||
@@ -353,14 +355,18 @@
|
||||
},
|
||||
"exportImport": {
|
||||
"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",
|
||||
"exporting": "Exporting...",
|
||||
"import": "Import Data",
|
||||
"import": "Select File",
|
||||
"importing": "Importing...",
|
||||
"selectFile": "Select File",
|
||||
"includeSensitive": "Include sensitive data",
|
||||
"sensitiveWarning": "Warning: This will include notification URLs (may contain passwords) in plain text in the export file.",
|
||||
"includeSensitive": "Include sensitive data (notification URLs)",
|
||||
"sensitiveWarning": "Notification URLs may contain passwords and will be stored in plain text.",
|
||||
"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.",
|
||||
"confirmImportWarning": "This action cannot be undone!",
|
||||
|
||||
@@ -3977,3 +3977,79 @@ h3 .reminder-icon.info-tooltip {
|
||||
margin-top: 0.5rem;
|
||||
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