feat: rename app to Last Signal
Rename service, UI, storage, and target-path configuration; clarify expiration cleanup behavior.
This commit is contained in:
@@ -15,9 +15,9 @@ RUN chmod +x /app/scripts/*.sh \
|
|||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
PORT=40100 \
|
PORT=40100 \
|
||||||
CONFIG_PATH=/app/config/default.json \
|
CONFIG_PATH=/app/config/default.json \
|
||||||
DATABASE_PATH=/data/keeper.sqlite \
|
DATABASE_PATH=/data/last-signal.sqlite \
|
||||||
TRIGGER_SCRIPT=/scripts/cleanup.sh \
|
TRIGGER_SCRIPT=/scripts/cleanup.sh \
|
||||||
COLLECTION_PATH=/home/zenn/Collection
|
TARGET_PATH=/home/zenn/Collection
|
||||||
|
|
||||||
EXPOSE 40100
|
EXPOSE 40100
|
||||||
|
|
||||||
|
|||||||
54
README.md
54
README.md
@@ -1,4 +1,4 @@
|
|||||||
# Collection Keeper
|
# Last Signal
|
||||||
|
|
||||||
개인 NAS 사용자를 위한 초경량 생존 확인 웹앱입니다.
|
개인 NAS 사용자를 위한 초경량 생존 확인 웹앱입니다.
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
가끔 방문해서 중앙 버튼을 누르면 확인 시간이 갱신되고, 일정 기간 동안 아무도 누르지 않으면 사용자가 지정한 스크립트가 실행됩니다.
|
가끔 방문해서 중앙 버튼을 누르면 확인 시간이 갱신되고, 일정 기간 동안 아무도 누르지 않으면 사용자가 지정한 스크립트가 실행됩니다.
|
||||||
|
|
||||||
Collection Keeper는 Collection 폴더를 직접 삭제하지 않습니다.
|
마지막 생존 확인 후 설정된 스크립트를 실행합니다. 기본 `scripts/cleanup.sh`는 `TARGET_PATH`로 지정한 폴더의 **내용 전체를 삭제**하며, 폴더 자체는 남깁니다. 기본 대상은 `/home/zenn/Collection`입니다.
|
||||||
|
|
||||||
## 주요 기능
|
## 주요 기능
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ http://localhost:40100
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
로컬 실행 시 SQLite 파일은 기본적으로 `data/keeper.sqlite`에 생성됩니다.
|
로컬 실행 시 SQLite 파일은 기본적으로 `data/last-signal.sqlite`에 생성됩니다.
|
||||||
|
|
||||||
## Docker 실행 가이드
|
## Docker 실행 가이드
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ docker compose up -d --build
|
|||||||
브라우저에서 접속:
|
브라우저에서 접속:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://localhost:40100
|
http://localhost:44092
|
||||||
```
|
```
|
||||||
|
|
||||||
상태 확인:
|
상태 확인:
|
||||||
@@ -99,7 +99,7 @@ docker compose down
|
|||||||
docker compose restart
|
docker compose restart
|
||||||
```
|
```
|
||||||
|
|
||||||
컨테이너와 네트워크를 중지해도 `keeper-data` 볼륨에 SQLite 데이터가 남습니다.
|
컨테이너와 네트워크를 중지해도 `last-signal-data` 볼륨에 SQLite 데이터가 남습니다.
|
||||||
|
|
||||||
## 상용/NAS 설치 가이드
|
## 상용/NAS 설치 가이드
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ NAS에서 오래 켜둘 때는 Docker Compose 실행을 권장합니다.
|
|||||||
1. 프로젝트 폴더를 NAS에 둡니다.
|
1. 프로젝트 폴더를 NAS에 둡니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /volume1/docker/collection-keeper
|
cd /volume1/docker/last-signal
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 설정을 확인합니다.
|
2. 설정을 확인합니다.
|
||||||
@@ -150,19 +150,19 @@ docker compose up -d --build
|
|||||||
6. 로그를 확인합니다.
|
6. 로그를 확인합니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f collection-keeper
|
docker compose logs -f last-signal
|
||||||
```
|
```
|
||||||
|
|
||||||
7. NAS 주소로 접속합니다.
|
7. NAS 주소로 접속합니다.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://NAS_IP:40100
|
http://NAS_IP:44092
|
||||||
```
|
```
|
||||||
|
|
||||||
예시:
|
예시:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://192.168.0.10:40100
|
http://192.168.0.10:44092
|
||||||
```
|
```
|
||||||
|
|
||||||
## 업데이트 가이드
|
## 업데이트 가이드
|
||||||
@@ -170,26 +170,26 @@ http://192.168.0.10:40100
|
|||||||
코드를 새 버전으로 바꾼 뒤 다시 빌드합니다.
|
코드를 새 버전으로 바꾼 뒤 다시 빌드합니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose down
|
docker compose down --remove-orphans
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
데이터를 유지하려면 `docker compose down -v`는 사용하지 마세요. `-v` 옵션은 SQLite 데이터가 들어 있는 Docker 볼륨까지 삭제합니다.
|
`--remove-orphans`는 이전 서비스 이름으로 남은 컨테이너를 정리합니다. 데이터를 유지하려면 `docker compose down -v`는 사용하지 마세요. `-v` 옵션은 SQLite 데이터가 들어 있는 Docker 볼륨까지 삭제합니다.
|
||||||
|
|
||||||
## 백업 가이드
|
## 백업 가이드
|
||||||
|
|
||||||
Docker Compose 기본 설정에서는 SQLite DB가 `keeper-data` 볼륨에 저장됩니다.
|
Docker Compose 기본 설정에서는 SQLite DB가 `last-signal-data` 볼륨에 저장됩니다.
|
||||||
|
|
||||||
볼륨 이름 확인:
|
볼륨 이름 확인:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker volume ls | grep keeper
|
docker volume ls | grep last-signal
|
||||||
```
|
```
|
||||||
|
|
||||||
로컬 실행에서는 `data/keeper.sqlite` 파일을 백업하면 됩니다.
|
로컬 실행에서는 `data/last-signal.sqlite` 파일을 백업하면 됩니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp data/keeper.sqlite keeper.backup.sqlite
|
cp data/last-signal.sqlite last-signal.backup.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
## 멀티 아키텍처 이미지 빌드
|
## 멀티 아키텍처 이미지 빌드
|
||||||
@@ -197,7 +197,7 @@ cp data/keeper.sqlite keeper.backup.sqlite
|
|||||||
ARM64와 x86_64 이미지를 함께 빌드하려면 Docker Buildx를 사용합니다.
|
ARM64와 x86_64 이미지를 함께 빌드하려면 Docker Buildx를 사용합니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 -t collection-keeper:latest .
|
docker buildx build --platform linux/amd64,linux/arm64 -t last-signal:latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
## 문제 해결
|
## 문제 해결
|
||||||
@@ -246,10 +246,10 @@ http://localhost:40101
|
|||||||
| `threshold_seconds` | `null` | 테스트용 초 단위 생존 확인 기간. 값이 있으면 `threshold_days`보다 우선합니다. |
|
| `threshold_seconds` | `null` | 테스트용 초 단위 생존 확인 기간. 값이 있으면 `threshold_days`보다 우선합니다. |
|
||||||
| `cooldown_seconds` | `1` | 생존 확인 인정 쿨다운 |
|
| `cooldown_seconds` | `1` | 생존 확인 인정 쿨다운 |
|
||||||
| `check_interval_seconds` | `60` | 스크립트 검사 주기 |
|
| `check_interval_seconds` | `60` | 스크립트 검사 주기 |
|
||||||
| `database_path` | `./data/keeper.sqlite` | SQLite 경로 |
|
| `database_path` | `./data/last-signal.sqlite` | SQLite 경로 |
|
||||||
| `trigger_script` | `./scripts/cleanup.sh` | 조건 충족 시 실행할 스크립트 |
|
| `trigger_script` | `./scripts/cleanup.sh` | 조건 충족 시 실행할 스크립트 |
|
||||||
| `background` | `random` | `none`, 배경 파일명, 또는 `random` |
|
| `background` | `random` | `none`, 배경 파일명, 또는 `random` |
|
||||||
| `collection_path` | `/home/zenn/Collection` | Collection 폴더 경로 |
|
| `target_path` | `/home/zenn/Collection` | 만료 시 스크립트가 처리할 대상 폴더 |
|
||||||
| `recent_traces_limit` | `5` | 최근 흔적 저장 조회 개수 |
|
| `recent_traces_limit` | `5` | 최근 흔적 저장 조회 개수 |
|
||||||
| `reset_token` | `""` | 초기화 API 보호용 토큰. 비어 있으면 토큰 없이 초기화 가능합니다. |
|
| `reset_token` | `""` | 초기화 API 보호용 토큰. 비어 있으면 토큰 없이 초기화 가능합니다. |
|
||||||
| `public_base_url` | `https://collection.sori.studio` | 메일 확인 링크에 사용할 공개 URL |
|
| `public_base_url` | `https://collection.sori.studio` | 메일 확인 링크에 사용할 공개 URL |
|
||||||
@@ -266,7 +266,7 @@ http://localhost:40101
|
|||||||
- `CHECK_INTERVAL_SECONDS`
|
- `CHECK_INTERVAL_SECONDS`
|
||||||
- `DATABASE_PATH`
|
- `DATABASE_PATH`
|
||||||
- `TRIGGER_SCRIPT`
|
- `TRIGGER_SCRIPT`
|
||||||
- `COLLECTION_PATH`
|
- `TARGET_PATH`
|
||||||
- `BACKGROUND`
|
- `BACKGROUND`
|
||||||
- `PORT`
|
- `PORT`
|
||||||
- `THRESHOLD_SECONDS`
|
- `THRESHOLD_SECONDS`
|
||||||
@@ -278,6 +278,8 @@ http://localhost:40101
|
|||||||
- `MAIL_TO`
|
- `MAIL_TO`
|
||||||
- `MAIL_CONFIRM_TOKEN`
|
- `MAIL_CONFIRM_TOKEN`
|
||||||
|
|
||||||
|
이전 설정과의 호환을 위해 `COLLECTION_PATH`와 `collection_path`도 읽습니다. 새 설정에는 `TARGET_PATH`와 `target_path`를 사용하세요.
|
||||||
|
|
||||||
## 화면 문구 커스터마이징
|
## 화면 문구 커스터마이징
|
||||||
|
|
||||||
화면에 보이는 글, 클릭 이모지, 클릭 메시지는 [`public/custom.js`](public/custom.js) 한 파일에서 바꿉니다.
|
화면에 보이는 글, 클릭 이모지, 클릭 메시지는 [`public/custom.js`](public/custom.js) 한 파일에서 바꿉니다.
|
||||||
@@ -285,7 +287,7 @@ http://localhost:40101
|
|||||||
코드를 몰라도 이 파일의 따옴표 안 문장과 이모지만 수정하면 됩니다.
|
코드를 몰라도 이 파일의 따옴표 안 문장과 이모지만 수정하면 됩니다.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
window.COLLECTION_KEEPER_CUSTOM = {
|
window.LAST_SIGNAL_CUSTOM = {
|
||||||
keeperTextLines: [
|
keeperTextLines: [
|
||||||
'가끔은 생각해.',
|
'가끔은 생각해.',
|
||||||
'',
|
'',
|
||||||
@@ -412,12 +414,14 @@ curl -X POST http://localhost:40100/api/reset \
|
|||||||
|
|
||||||
스크립트에는 다음 환경 변수가 전달됩니다.
|
스크립트에는 다음 환경 변수가 전달됩니다.
|
||||||
|
|
||||||
- `COLLECTION_PATH`
|
- `TARGET_PATH`
|
||||||
- `KEEPER_SCRIPT`
|
- `TRIGGER_SCRIPT`
|
||||||
|
|
||||||
|
기존 스크립트 호환을 위해 `COLLECTION_PATH`와 `KEEPER_SCRIPT`도 같은 값으로 전달됩니다. 새 스크립트에는 새 이름을 사용하세요.
|
||||||
|
|
||||||
## 안전 원칙
|
## 안전 원칙
|
||||||
|
|
||||||
Collection Keeper는 다음 역할만 수행합니다.
|
Last Signal은 다음 역할을 수행합니다.
|
||||||
|
|
||||||
1. 마지막 확인 시간 관리
|
1. 마지막 확인 시간 관리
|
||||||
2. 통계 관리
|
2. 통계 관리
|
||||||
@@ -425,7 +429,7 @@ Collection Keeper는 다음 역할만 수행합니다.
|
|||||||
4. 조건 충족 여부 판단
|
4. 조건 충족 여부 판단
|
||||||
5. 사용자 지정 스크립트 실행
|
5. 사용자 지정 스크립트 실행
|
||||||
|
|
||||||
파일 삭제, 암호화, 정리 같은 실제 작업은 반드시 사용자 스크립트에서 구현해야 합니다.
|
파일 처리 동작은 실행 스크립트에 달려 있습니다. 기본 `scripts/cleanup.sh`는 `TARGET_PATH` 폴더의 내용을 전부 삭제합니다. 다른 작업을 원하면 해당 스크립트를 수정하고, 실제 데이터가 아닌 임시 폴더에서 먼저 검증하세요.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
@@ -438,7 +442,7 @@ Collection Keeper는 다음 역할만 수행합니다.
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
- keeper-data:/data
|
- last-signal-data:/data
|
||||||
- ./scripts:/scripts:ro
|
- ./scripts:/scripts:ro
|
||||||
- /home/zenn/Collection:/home/zenn/Collection
|
- /home/zenn/Collection:/home/zenn/Collection
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
"threshold_seconds": null,
|
"threshold_seconds": null,
|
||||||
"cooldown_seconds": 1,
|
"cooldown_seconds": 1,
|
||||||
"check_interval_seconds": 60,
|
"check_interval_seconds": 60,
|
||||||
"database_path": "./data/keeper.sqlite",
|
"database_path": "./data/last-signal.sqlite",
|
||||||
"trigger_script": "./scripts/cleanup.sh",
|
"trigger_script": "./scripts/cleanup.sh",
|
||||||
"background": "random",
|
"background": "random",
|
||||||
"collection_path": "/home/zenn/Collection",
|
"target_path": "/home/zenn/Collection",
|
||||||
"port": 40100,
|
"port": 40100,
|
||||||
"recent_traces_limit": 5,
|
"recent_traces_limit": 5,
|
||||||
"reset_token": "",
|
"reset_token": "",
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
services:
|
services:
|
||||||
collection-keeper:
|
last-signal:
|
||||||
build: .
|
build: .
|
||||||
image: collection-keeper:latest
|
image: last-signal:latest
|
||||||
container_name: collection-keeper
|
container_name: last-signal
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "40100:40100"
|
- "44092:44092"
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Seoul
|
TZ: Asia/Seoul
|
||||||
THRESHOLD_DAYS: 90
|
THRESHOLD_DAYS: 90
|
||||||
@@ -13,10 +13,10 @@ services:
|
|||||||
# THRESHOLD_SECONDS: 10
|
# THRESHOLD_SECONDS: 10
|
||||||
COOLDOWN_SECONDS: 1
|
COOLDOWN_SECONDS: 1
|
||||||
CHECK_INTERVAL_SECONDS: 60
|
CHECK_INTERVAL_SECONDS: 60
|
||||||
PORT: 40100
|
PORT: 44092
|
||||||
DATABASE_PATH: /data/keeper.sqlite
|
DATABASE_PATH: /data/last-signal.sqlite
|
||||||
TRIGGER_SCRIPT: /scripts/cleanup.sh
|
TRIGGER_SCRIPT: /scripts/cleanup.sh
|
||||||
COLLECTION_PATH: /home/zenn/Collection
|
TARGET_PATH: /home/zenn/Collection
|
||||||
BACKGROUND: random
|
BACKGROUND: random
|
||||||
RESET_TOKEN: ${RESET_TOKEN:?Set RESET_TOKEN in .env}
|
RESET_TOKEN: ${RESET_TOKEN:?Set RESET_TOKEN in .env}
|
||||||
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-https://collection.sori.studio}
|
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-https://collection.sori.studio}
|
||||||
@@ -26,11 +26,11 @@ services:
|
|||||||
MAIL_TO: ${MAIL_TO:-zenn.message@gmail.com}
|
MAIL_TO: ${MAIL_TO:-zenn.message@gmail.com}
|
||||||
MAIL_CONFIRM_TOKEN: ${MAIL_CONFIRM_TOKEN:-}
|
MAIL_CONFIRM_TOKEN: ${MAIL_CONFIRM_TOKEN:-}
|
||||||
volumes:
|
volumes:
|
||||||
- keeper-data:/data
|
- last-signal-data:/data
|
||||||
- ./config/default.json:/app/config/default.json:ro
|
- ./config/default.json:/app/config/default.json:ro
|
||||||
- ./scripts:/scripts:ro
|
- ./scripts:/scripts:ro
|
||||||
- ./assets/backgrounds:/app/assets/backgrounds:ro
|
- ./assets/backgrounds:/app/assets/backgrounds:ro
|
||||||
- /home/zenn/Collection:/home/zenn/Collection
|
- /home/zenn/Collection:/home/zenn/Collection
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
keeper-data:
|
last-signal-data:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Collection Keeper 설치 가이드
|
# Last Signal 설치 가이드
|
||||||
|
|
||||||
이 문서는 컴퓨터를 잘 모르는 분도 따라할 수 있도록 작성했습니다.
|
이 문서는 컴퓨터를 잘 모르는 분도 따라할 수 있도록 작성했습니다.
|
||||||
|
|
||||||
@@ -12,16 +12,18 @@
|
|||||||
|
|
||||||
## 1. 이 프로그램이 하는 일
|
## 1. 이 프로그램이 하는 일
|
||||||
|
|
||||||
Collection Keeper는 웹페이지에 들어와서 가끔 클릭해 주는 작은 서비스입니다.
|
Last Signal은 웹페이지에 들어와서 가끔 생존 확인 버튼을 눌러주는 서비스입니다.
|
||||||
|
|
||||||
- 클릭하면 "아직 살아있음" 기록이 남습니다.
|
- 클릭하면 "아직 살아있음" 기록이 남습니다.
|
||||||
- 일정 기간 동안 아무도 클릭하지 않으면, 미리 준비한 스크립트가 실행됩니다.
|
- 일정 기간 동안 아무도 클릭하지 않으면, 미리 준비한 스크립트가 실행됩니다.
|
||||||
- NAS에서는 `/home/zenn/Collection` 폴더를 삭제하도록 설정되어 있습니다.
|
- 확인이 설정된 기간 동안 없으면 지정된 스크립트를 실행합니다.
|
||||||
|
- 기본 스크립트는 `/home/zenn/Collection` 폴더 **안의 내용 전체를 삭제**합니다. 폴더 자체는 남습니다.
|
||||||
|
|
||||||
중요:
|
중요:
|
||||||
|
|
||||||
- 웹앱이 직접 폴더를 지우지 않습니다.
|
- 웹앱이 직접 폴더를 지우지 않습니다.
|
||||||
- 실제 삭제는 `scripts/cleanup.sh`가 담당합니다.
|
- 웹앱이 직접 파일을 지우지는 않습니다. 기본 `scripts/cleanup.sh`가 `TARGET_PATH`의 내용을 삭제합니다.
|
||||||
|
- `docker-compose.yml`에서 호스트 폴더를 컨테이너에 마운트하고, `TARGET_PATH`에는 컨테이너 쪽 경로를 지정합니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -173,7 +175,7 @@ config/default.json
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `threshold_days` | `90` | 며칠 동안 클릭이 없으면 스크립트 실행 |
|
| `threshold_days` | `90` | 며칠 동안 클릭이 없으면 스크립트 실행 |
|
||||||
| `cooldown_seconds` | `1` | 클릭 인정 최소 간격 |
|
| `cooldown_seconds` | `1` | 클릭 인정 최소 간격 |
|
||||||
| `collection_path` | `/home/zenn/Collection` | 삭제 대상 폴더 |
|
| `target_path` | `/home/zenn/Collection` | 만료 시 스크립트가 처리할 대상 폴더 |
|
||||||
| `trigger_script` | `./scripts/cleanup.sh` | 실행할 스크립트 |
|
| `trigger_script` | `./scripts/cleanup.sh` | 실행할 스크립트 |
|
||||||
| `port` | `40100` | 웹 접속 포트 |
|
| `port` | `40100` | 웹 접속 포트 |
|
||||||
|
|
||||||
@@ -207,13 +209,13 @@ docker compose up -d --build
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps
|
docker compose ps
|
||||||
docker compose logs -f collection-keeper
|
docker compose logs -f last-signal
|
||||||
```
|
```
|
||||||
|
|
||||||
브라우저 접속:
|
브라우저 접속:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://NAS_IP:40100
|
http://NAS_IP:44092
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -228,10 +230,10 @@ http://NAS_IP:40100
|
|||||||
| `threshold_seconds` | `null` | 테스트용 초 단위 타이머. 값이 있으면 일수보다 우선 |
|
| `threshold_seconds` | `null` | 테스트용 초 단위 타이머. 값이 있으면 일수보다 우선 |
|
||||||
| `cooldown_seconds` | `1` | 클릭 인정 쿨다운 |
|
| `cooldown_seconds` | `1` | 클릭 인정 쿨다운 |
|
||||||
| `check_interval_seconds` | `60` | 스크립트 검사 주기 |
|
| `check_interval_seconds` | `60` | 스크립트 검사 주기 |
|
||||||
| `database_path` | `./data/keeper.sqlite` | SQLite 경로 |
|
| `database_path` | `./data/last-signal.sqlite` | SQLite 경로 |
|
||||||
| `trigger_script` | `./scripts/cleanup.sh` | 만료 시 실행 스크립트 |
|
| `trigger_script` | `./scripts/cleanup.sh` | 만료 시 실행 스크립트 |
|
||||||
| `background` | `random` | 배경 이미지 설정 |
|
| `background` | `random` | 배경 이미지 설정 |
|
||||||
| `collection_path` | `/home/zenn/Collection` | Collection 폴더 |
|
| `target_path` | `/home/zenn/Collection` | 만료 시 스크립트가 처리할 대상 폴더 |
|
||||||
| `port` | `40100` | 웹 포트 |
|
| `port` | `40100` | 웹 포트 |
|
||||||
| `recent_traces_limit` | `5` | 최근 흔적 개수 |
|
| `recent_traces_limit` | `5` | 최근 흔적 개수 |
|
||||||
| `reset_token` | `""` | 초기화 API 보호 토큰 |
|
| `reset_token` | `""` | 초기화 API 보호 토큰 |
|
||||||
@@ -264,7 +266,7 @@ http://NAS_IP:40100
|
|||||||
- `PORT`
|
- `PORT`
|
||||||
- `DATABASE_PATH`
|
- `DATABASE_PATH`
|
||||||
- `TRIGGER_SCRIPT`
|
- `TRIGGER_SCRIPT`
|
||||||
- `COLLECTION_PATH`
|
- `TARGET_PATH`
|
||||||
- `BACKGROUND`
|
- `BACKGROUND`
|
||||||
- `RESET_TOKEN`
|
- `RESET_TOKEN`
|
||||||
- `TZ`
|
- `TZ`
|
||||||
@@ -284,10 +286,12 @@ http://NAS_IP:40100
|
|||||||
```bash
|
```bash
|
||||||
cd /home/zenn/docker/collection.sori.studio
|
cd /home/zenn/docker/collection.sori.studio
|
||||||
git pull
|
git pull
|
||||||
docker compose down
|
docker compose down --remove-orphans
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
새 데이터 볼륨 이름은 `last-signal-data`입니다. 기존 `keeper-data` 볼륨은 새 설정에서 사용하지 않습니다.
|
||||||
|
|
||||||
개발 서버:
|
개발 서버:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "collection-keeper",
|
"name": "last-signal",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "collection-keeper",
|
"name": "last-signal",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "collection-keeper",
|
"name": "last-signal",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "A lightweight survival check web app for personal NAS users",
|
"description": "A lightweight survival check web app for personal NAS users",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const custom = window.COLLECTION_KEEPER_CUSTOM ?? {};
|
const custom = window.LAST_SIGNAL_CUSTOM ?? {};
|
||||||
const activeEmojis = custom.emojis?.length ? custom.emojis : ['✨'];
|
const activeEmojis = custom.emojis?.length ? custom.emojis : ['✨'];
|
||||||
const activeClickMessages = custom.clickMessages?.length ? custom.clickMessages : ['기록은 남겨둘게.'];
|
const activeClickMessages = custom.clickMessages?.length ? custom.clickMessages : ['기록은 남겨둘게.'];
|
||||||
const timeMessageGroups = {
|
const timeMessageGroups = {
|
||||||
@@ -23,7 +23,7 @@ const NOTICE_THRESHOLDS = {
|
|||||||
const SEOUL_TIME_ZONE = 'Asia/Seoul';
|
const SEOUL_TIME_ZONE = 'Asia/Seoul';
|
||||||
const KEEPER_TEXT_LINES = custom.keeperTextLines?.length
|
const KEEPER_TEXT_LINES = custom.keeperTextLines?.length
|
||||||
? custom.keeperTextLines
|
? custom.keeperTextLines
|
||||||
: ['Collection Keeper'];
|
: ['Last Signal'];
|
||||||
const FAREWELL_TEXT_LINES = custom.farewellTextLines?.length
|
const FAREWELL_TEXT_LINES = custom.farewellTextLines?.length
|
||||||
? custom.farewellTextLines
|
? custom.farewellTextLines
|
||||||
: ['안녕.', '고마웠어.'];
|
: ['안녕.', '고마웠어.'];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Collection Keeper 사용자 설정 파일
|
// Last Signal 사용자 설정 파일
|
||||||
// 이 파일만 수정하면 화면 문구, 클릭 이모지, 클릭 메시지를 바꿀 수 있습니다.
|
// 이 파일만 수정하면 화면 문구, 클릭 이모지, 클릭 메시지를 바꿀 수 있습니다.
|
||||||
|
|
||||||
window.COLLECTION_KEEPER_CUSTOM = {
|
window.LAST_SIGNAL_CUSTOM = {
|
||||||
// 오른쪽 아래에 표시할 글입니다. 줄을 추가하거나 지우면 화면에도 그대로 반영됩니다.
|
// 오른쪽 아래에 표시할 글입니다. 줄을 추가하거나 지우면 화면에도 그대로 반영됩니다.
|
||||||
keeperTextLines: [
|
keeperTextLines: [
|
||||||
'가끔은 생각해.',
|
'가끔은 생각해.',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Collection Keeper</title>
|
<title>Last Signal</title>
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
TARGET="${COLLECTION_PATH:-/home/zenn/Collection}"
|
TARGET="${TARGET_PATH:-/home/zenn/Collection}"
|
||||||
|
|
||||||
case "$TARGET" in
|
case "$TARGET" in
|
||||||
/home/zenn/Collection) ;;
|
/*) ;;
|
||||||
*)
|
*)
|
||||||
echo "[collection-keeper] refused unsafe path: $TARGET"
|
echo "[last-signal] refused non-absolute target path: $TARGET"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ ! -e "$TARGET" ]; then
|
if [ -L "$TARGET" ]; then
|
||||||
echo "[collection-keeper] target not found: $TARGET"
|
echo "[last-signal] refused symbolic-link target: $TARGET"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$TARGET" ]; then
|
||||||
|
if [ -e "$TARGET" ]; then
|
||||||
|
echo "[last-signal] refused non-directory target: $TARGET"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[last-signal] target directory not found: $TARGET"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf -- "$TARGET"
|
TARGET_REAL="$(CDPATH= cd "$TARGET" && pwd -P)"
|
||||||
echo "[collection-keeper] deleted: $TARGET"
|
if [ "$TARGET_REAL" = "/" ]; then
|
||||||
|
echo "[last-signal] refused filesystem root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
find "$TARGET_REAL" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
|
||||||
|
echo "[last-signal] deleted contents of: $TARGET_REAL"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
echo "[collection-keeper] encrypt.sh example script"
|
echo "[last-signal] encrypt.sh example script"
|
||||||
echo "[collection-keeper] COLLECTION_PATH=${COLLECTION_PATH:-/home/zenn/Collection}"
|
echo "[last-signal] TARGET_PATH=${TARGET_PATH:-/home/zenn/Collection}"
|
||||||
echo "[collection-keeper] Encrypt sensitive files here."
|
echo "[last-signal] Encrypt sensitive files here."
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
echo "[collection-keeper] warning.sh example script"
|
echo "[last-signal] warning.sh example script"
|
||||||
echo "[collection-keeper] COLLECTION_PATH=${COLLECTION_PATH:-/home/zenn/Collection}"
|
echo "[last-signal] TARGET_PATH=${TARGET_PATH:-/home/zenn/Collection}"
|
||||||
echo "[collection-keeper] Send a warning notification here."
|
echo "[last-signal] Send a warning notification here."
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -25,10 +25,15 @@ function loadConfig() {
|
|||||||
check_interval_seconds: Number(
|
check_interval_seconds: Number(
|
||||||
process.env.CHECK_INTERVAL_SECONDS ?? fileConfig.check_interval_seconds ?? 60
|
process.env.CHECK_INTERVAL_SECONDS ?? fileConfig.check_interval_seconds ?? 60
|
||||||
),
|
),
|
||||||
database_path: process.env.DATABASE_PATH ?? fileConfig.database_path ?? './data/keeper.sqlite',
|
database_path: process.env.DATABASE_PATH ?? fileConfig.database_path ?? './data/last-signal.sqlite',
|
||||||
trigger_script: process.env.TRIGGER_SCRIPT ?? fileConfig.trigger_script ?? './scripts/cleanup.sh',
|
trigger_script: process.env.TRIGGER_SCRIPT ?? fileConfig.trigger_script ?? './scripts/cleanup.sh',
|
||||||
background: process.env.BACKGROUND ?? fileConfig.background ?? 'random',
|
background: process.env.BACKGROUND ?? fileConfig.background ?? 'random',
|
||||||
collection_path: process.env.COLLECTION_PATH ?? fileConfig.collection_path ?? '/home/zenn/Collection',
|
target_path:
|
||||||
|
process.env.TARGET_PATH ??
|
||||||
|
process.env.COLLECTION_PATH ??
|
||||||
|
fileConfig.target_path ??
|
||||||
|
fileConfig.collection_path ??
|
||||||
|
'/home/zenn/Collection',
|
||||||
port: Number(process.env.PORT ?? fileConfig.port ?? 40100),
|
port: Number(process.env.PORT ?? fileConfig.port ?? 40100),
|
||||||
recent_traces_limit: Number(
|
recent_traces_limit: Number(
|
||||||
process.env.RECENT_TRACES_LIMIT ?? fileConfig.recent_traces_limit ?? 5
|
process.env.RECENT_TRACES_LIMIT ?? fileConfig.recent_traces_limit ?? 5
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ function renderConfirmSuccessPage() {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Collection Keeper</title>
|
<title>Last Signal</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -35,7 +35,7 @@ function renderConfirmSuccessPage() {
|
|||||||
<main>
|
<main>
|
||||||
<h1>❤️ 확인 완료</h1>
|
<h1>❤️ 확인 완료</h1>
|
||||||
<p>다시 와줘서 고마워.</p>
|
<p>다시 와줘서 고마워.</p>
|
||||||
<p>컬렉션은 계속 안전하게 보관될 거야.</p>
|
<p>설정한 폴더는 다음 확인 전까지 그대로 있어.</p>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
@@ -47,7 +47,7 @@ function renderConfirmErrorPage(message) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Collection Keeper</title>
|
<title>Last Signal</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ function buildStatus(db, config) {
|
|||||||
threshold_days: config.threshold_days,
|
threshold_days: config.threshold_days,
|
||||||
threshold_seconds: config.threshold_seconds,
|
threshold_seconds: config.threshold_seconds,
|
||||||
cooldown_seconds: config.cooldown_seconds,
|
cooldown_seconds: config.cooldown_seconds,
|
||||||
collection_path: config.collection_path,
|
target_path: config.target_path,
|
||||||
recent_traces: getRecentEvents(db, config.recent_traces_limit).map((event) => ({
|
recent_traces: getRecentEvents(db, config.recent_traces_limit).map((event) => ({
|
||||||
emoji: event.emoji,
|
emoji: event.emoji,
|
||||||
time: formatTime(new Date(event.created_at)),
|
time: formatTime(new Date(event.created_at)),
|
||||||
|
|||||||
@@ -2,21 +2,21 @@ const nodemailer = require('nodemailer');
|
|||||||
|
|
||||||
const EMAIL_TEMPLATES = {
|
const EMAIL_TEMPLATES = {
|
||||||
60: {
|
60: {
|
||||||
subject: '[Collection Keeper] 오랜만이네.',
|
subject: '[Last Signal] 오랜만이네.',
|
||||||
headline: '오랜만이네.',
|
headline: '오랜만이네.',
|
||||||
body: [
|
body: [
|
||||||
'안녕.',
|
'안녕.',
|
||||||
'',
|
'',
|
||||||
'마지막 확인 이후 60일이 지났어.',
|
'마지막 확인 이후 60일이 지났어.',
|
||||||
'',
|
'',
|
||||||
'컬렉션은 아직 안전하게 보관되고 있어.',
|
'설정한 폴더는 아직 그대로 있어.',
|
||||||
'',
|
'',
|
||||||
'혹시 잊고 있었더라도 괜찮아.',
|
'혹시 잊고 있었더라도 괜찮아.',
|
||||||
'아래 버튼을 누르면 바로 확인할 수 있어.',
|
'아래 버튼을 누르면 바로 확인할 수 있어.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
75: {
|
75: {
|
||||||
subject: '[Collection Keeper] 조금 걱정하고 있어.',
|
subject: '[Last Signal] 조금 걱정하고 있어.',
|
||||||
headline: '조금 걱정하고 있어.',
|
headline: '조금 걱정하고 있어.',
|
||||||
body: [
|
body: [
|
||||||
'안녕.',
|
'안녕.',
|
||||||
@@ -26,11 +26,11 @@ const EMAIL_TEMPLATES = {
|
|||||||
'아직은 괜찮지만,',
|
'아직은 괜찮지만,',
|
||||||
'생각보다 긴 시간이 흘렀네.',
|
'생각보다 긴 시간이 흘렀네.',
|
||||||
'',
|
'',
|
||||||
'컬렉션은 계속 기다리고 있어.',
|
'설정한 폴더는 아직 그대로 있어.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
89: {
|
89: {
|
||||||
subject: '[Collection Keeper] 정말 마지막 안내야.',
|
subject: '[Last Signal] 정말 마지막 안내야.',
|
||||||
headline: '정말 마지막 안내야.',
|
headline: '정말 마지막 안내야.',
|
||||||
body: [
|
body: [
|
||||||
'안녕.',
|
'안녕.',
|
||||||
@@ -139,7 +139,7 @@ async function sendAlertEmail(config, milestone, status) {
|
|||||||
const content = buildEmailContent(milestone, status, confirmUrl);
|
const content = buildEmailContent(milestone, status, confirmUrl);
|
||||||
|
|
||||||
await transport.sendMail({
|
await transport.sendMail({
|
||||||
from: `"Collection Keeper" <${config.mail_smtp_user}>`,
|
from: `"Last Signal" <${config.mail_smtp_user}>`,
|
||||||
to: config.mail_to,
|
to: config.mail_to,
|
||||||
subject: content.subject,
|
subject: content.subject,
|
||||||
text: content.text,
|
text: content.text,
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ async function evaluateTrigger() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[keeper] Threshold exceeded. Running script: ${config.trigger_script}`);
|
console.log(`[keeper] Threshold exceeded. Running script: ${config.trigger_script}`);
|
||||||
const result = await runTriggerScript(config.trigger_script, config.collection_path);
|
const result = await runTriggerScript(config.trigger_script, config.target_path);
|
||||||
markTriggerExecuted(db, config.trigger_script, result.success, result.output);
|
markTriggerExecuted(db, config.trigger_script, result.success, result.output);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
function runTriggerScript(scriptPath, collectionPath) {
|
function runTriggerScript(scriptPath, targetPath) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!fs.existsSync(scriptPath)) {
|
if (!fs.existsSync(scriptPath)) {
|
||||||
resolve({
|
resolve({
|
||||||
@@ -14,7 +14,9 @@ function runTriggerScript(scriptPath, collectionPath) {
|
|||||||
const child = spawn('sh', [scriptPath], {
|
const child = spawn('sh', [scriptPath], {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
COLLECTION_PATH: collectionPath,
|
TARGET_PATH: targetPath,
|
||||||
|
COLLECTION_PATH: targetPath,
|
||||||
|
TRIGGER_SCRIPT: scriptPath,
|
||||||
KEEPER_SCRIPT: scriptPath,
|
KEEPER_SCRIPT: scriptPath,
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
|||||||
Reference in New Issue
Block a user