Skip to content

Commit 4b70bc6

Browse files
m4dm4rtig4nClément VALENTINclaudesemantic-release-bot
authored
Release 1.2.0 (#87)
* fix(api): resolve all ruff linting errors - Add TYPE_CHECKING imports for SQLAlchemy forward references - Fix SQLAlchemy boolean comparisons (use .is_(True) instead of == True) - Fix SQLAlchemy None comparisons (use .is_(None) instead of == None) - Remove unused imports and variables - Remove f-strings without placeholders 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(api): resolve all 204 mypy type errors - Add TYPE_CHECKING imports for forward references - Fix implicit Optional (str = None → str | None = None) - Add missing return type annotations - Use cast() for Any returns - Fix SQLAlchemy ORM assignments with type ignore - Fix variable reuse confusion (renamed result variables) - Add assertions for None checks before returns - Fix AsyncGenerator return type for database session All 62 source files now pass mypy --ignore-missing-imports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(api): remove unused imports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(ci): add pre-commit hooks for linting Add pre-commit configuration with hooks for: - ruff (lint + format) for backend Python code - mypy type checking for backend - eslint for frontend TypeScript/React - tsc type checking for frontend 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): add mypy to dependency-groups for CI type checking The CI workflow uses `uv sync` which relies on [dependency-groups], not [project.optional-dependencies]. Added mypy and cleaned up duplicate dependencies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.1.0-dev.1 [skip ci] * fix(ci): use semantic-release action for proper GitHub outputs The previous npx semantic-release command did not set GitHub Actions outputs, causing Docker builds to be skipped. Using the official action ensures new_release_published and new_release_version are properly exposed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): add extra_plugins for semantic-release action The cycjimmy action uses its own semantic-release installation, so additional plugins must be specified via extra_plugins input. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.1.0-dev.2 [skip ci] * fix(helm): correct postgres/valkey condition and configmap references - Fix Chart.yaml: change postgresql.enabled to postgres.enabled condition - Fix configmap.yaml: update redis references to valkey 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.1.0-dev.3 [skip ci] * feat(helm): migrate from Redis to Valkey - Replace CloudPirates Redis subchart with Valkey 0.13.0 - Update _helpers.tpl with valkey.* helpers - Fix secret key to use 'password' (CloudPirates default) - Update values.yaml for Valkey configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.1.0-dev.4 [skip ci] * feat(ci): separate CI/CD pipelines for apps and Helm chart - Add paths-ignore to release.yml to exclude helm/ changes - Create helm-ci.yml for chart validation (lint + template) on PRs - Refactor helm-release.yml with independent semantic-release - Add .releaserc-helm.yaml for chart-specific versioning (helm-* tags) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.1.0-dev.5 [skip ci] * fix(ci): trigger release only on apps changes Change from paths-ignore to paths for explicit control: - apps/** (backend + frontend code) - package.json, package-lock.json - .releaserc.yaml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): use helm/vX.X.X tags without GitHub releases - Change tag format from helm-X.X.X to helm/vX.X.X - Remove @semantic-release/github plugin (tags only, no releases) - Update workflow summaries to show new tag format 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(ci): use config swap instead of extends for helm release The extends option tries to npm install the file path. Instead, temporarily swap .releaserc.yaml with helm config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(helm): release 1.0.0-dev.1 [skip ci] * fix(valkey): update existingSecretKey to existingSecretPasswordKey in values.yaml and helpers.tpl * chore(helm): release 1.0.0-dev.2 [skip ci] * fix(ci): remove 'v' prefix from helm tags Tags will now be helm/X.X.X instead of helm/vX.X.X 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(web): handle nested arrays in offpeak_hours parsing The offpeak_hours field can be stored as: - Array of strings: ["22:00-06:00"] - Object with string values: {"default": "22h00-06h00"} - Object with nested arrays: {"ranges": ["22:00-06:00"]} The last format (created by backend when syncing with Enedis) was causing "W.match is not a function" errors because Object.values() returns the array as-is, not its string contents. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.1.0-dev.6 [skip ci] * fix(web): sync OfferSelector state on page navigation Simplify the effect that syncs selector dropdowns with the selected offer. Previously, tracking lastSyncedOfferId in state caused sync issues when navigating away and back to the Dashboard - the energy provider would not appear without a manual page refresh. Now the selectors are always synchronized when selectedOffer becomes available, ensuring proper display after page navigation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.1.0-dev.7 [skip ci] * ci: auto-sync develop with main after merge Add workflow to automatically merge main back into develop after each push to main. This prevents PR diffs from showing old commits that were already merged via squash. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(web): add JSON download button for API credentials Add a download button on the signup success page to save client_id and client_secret as a JSON file (myelectricaldata-credentials.json). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(web): make login button full width on signup success 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.2.0-dev.1 [skip ci] * feat(web): mask client_secret and update warning message - Client secret is now hidden by default with show/hide toggle - Updated warning message to clarify that client secret can be regenerated from account settings if lost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore(release): 1.2.0-dev.2 [skip ci] * fix(ci): disable ARM64 build by default to speed up CI - Add build_arm64 input parameter (default: false) - Build only linux/amd64 by default (fast) - ARM64 can be enabled manually via workflow_dispatch - Add 30 minute timeout to prevent stuck builds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Clément VALENTIN <clement.valentin@blacktiger.tech> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
1 parent 17c66d4 commit 4b70bc6

File tree

11 files changed

+182
-41
lines changed

11 files changed

+182
-41
lines changed

.github/workflows/docker-release.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ on:
99
version:
1010
description: 'Version tag (e.g., 1.0.0 or 1.0.0-dev.1)'
1111
required: true
12+
build_arm64:
13+
description: 'Build ARM64 images (slow, uses QEMU emulation)'
14+
type: boolean
15+
default: false
1216

1317
env:
1418
REGISTRY: ghcr.io
@@ -62,11 +66,21 @@ jobs:
6266
6367
echo "tags=$TAGS" >> $GITHUB_OUTPUT
6468
69+
- name: Determine platforms
70+
id: platforms
71+
run: |
72+
if [[ "${{ github.event.inputs.build_arm64 }}" == "true" ]]; then
73+
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
74+
else
75+
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
76+
fi
77+
6578
- name: Build and push ${{ matrix.image }}
6679
uses: docker/build-push-action@v6
80+
timeout-minutes: 30
6781
with:
6882
context: ${{ matrix.context }}
69-
platforms: linux/amd64,linux/arm64
83+
platforms: ${{ steps.platforms.outputs.platforms }}
7084
push: true
7185
tags: ${{ steps.tags.outputs.tags }}
7286
cache-from: type=gha,scope=${{ matrix.image }}

.github/workflows/release.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ on:
1111
- 'package-lock.json'
1212
- '.releaserc.yaml'
1313
workflow_dispatch:
14+
inputs:
15+
build_arm64:
16+
description: 'Build ARM64 images (slow, uses QEMU emulation)'
17+
type: boolean
18+
default: false
1419

1520
# Prevent concurrent releases
1621
concurrency:
@@ -193,11 +198,21 @@ jobs:
193198
echo "tags=$TAGS" >> $GITHUB_OUTPUT
194199
echo "Tags: $TAGS"
195200
201+
- name: Determine platforms
202+
id: platforms
203+
run: |
204+
if [[ "${{ github.event.inputs.build_arm64 }}" == "true" ]]; then
205+
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
206+
else
207+
echo "platforms=linux/amd64" >> $GITHUB_OUTPUT
208+
fi
209+
196210
- name: Build and push ${{ matrix.image }}
197211
uses: docker/build-push-action@v6
212+
timeout-minutes: 30
198213
with:
199214
context: ${{ matrix.context }}
200-
platforms: linux/amd64,linux/arm64
215+
platforms: ${{ steps.platforms.outputs.platforms }}
201216
push: true
202217
tags: ${{ steps.tags.outputs.tags }}
203218
cache-from: type=gha,scope=${{ matrix.image }}

.github/workflows/sync-develop.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Sync develop with main
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
sync:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
token: ${{ secrets.GITHUB_TOKEN }}
17+
18+
- name: Configure Git
19+
run: |
20+
git config user.name "github-actions[bot]"
21+
git config user.email "github-actions[bot]@users.noreply.github.com"
22+
23+
- name: Sync develop with main
24+
run: |
25+
git checkout develop
26+
git merge origin/main -m "chore: sync develop with main [skip ci]"
27+
git push origin develop

CHANGELOG.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,54 @@
11
# Changelog
22

3-
## [1.1.0](https://github.com/MyElectricalData/myelectricaldata_new/compare/1.0.0...1.1.0) (2025-12-18)
3+
## [1.2.0-dev.2](https://github.com/MyElectricalData/myelectricaldata_new/compare/1.2.0-dev.1...1.2.0-dev.2) (2025-12-19)
44

55
### Features
66

7-
* **ci:** add build validation before semantic-release ([f261fe7](https://github.com/MyElectricalData/myelectricaldata_new/commit/f261fe7faee82629eeb519ea6314fdab8fe322df))
7+
* **web:** mask client_secret and update warning message ([8824238](https://github.com/MyElectricalData/myelectricaldata_new/commit/8824238120167c9fd5ec17cc9a423e70f3e8f652))
8+
9+
## [1.2.0-dev.1](https://github.com/MyElectricalData/myelectricaldata_new/compare/1.1.0...1.2.0-dev.1) (2025-12-19)
10+
11+
### Features
12+
13+
* **ci:** add pre-commit hooks for linting ([c1614a9](https://github.com/MyElectricalData/myelectricaldata_new/commit/c1614a9e813a62328ed20eea777ea47b62843f45))
14+
* **ci:** separate CI/CD pipelines for apps and Helm chart ([6480760](https://github.com/MyElectricalData/myelectricaldata_new/commit/64807609ad5f7dc2c8c14701e569d67e6fe6573d))
15+
* **helm:** migrate from Redis to Valkey ([5dd2ada](https://github.com/MyElectricalData/myelectricaldata_new/commit/5dd2adae748dd4a7e5fa6d2185162c7734823704))
16+
* **web:** add JSON download button for API credentials ([ed5bc85](https://github.com/MyElectricalData/myelectricaldata_new/commit/ed5bc85d70b080793dd3c9ea0a09f8eed9765a3d))
17+
18+
### Bug Fixes
19+
20+
* **api:** remove unused imports ([6da4bf0](https://github.com/MyElectricalData/myelectricaldata_new/commit/6da4bf0955de2b360fea0cb3f660b74cf25c0df0))
21+
* **api:** resolve all 204 mypy type errors ([3e45d5b](https://github.com/MyElectricalData/myelectricaldata_new/commit/3e45d5b8e38c663216ddcc69dacad6035a7f1d20))
22+
* **api:** resolve all ruff linting errors ([d3366b3](https://github.com/MyElectricalData/myelectricaldata_new/commit/d3366b38bf1ece18179c9ad1c8ccdfa9899abde6))
23+
* **ci:** add extra_plugins for semantic-release action ([5a1561f](https://github.com/MyElectricalData/myelectricaldata_new/commit/5a1561f2e7fc4e2b1e576fd3163a44cccb716ba4))
24+
* **ci:** add mypy to dependency-groups for CI type checking ([717f99d](https://github.com/MyElectricalData/myelectricaldata_new/commit/717f99d1aaf1634780d5c8d80b1c8cbefcec9984))
25+
* **ci:** remove 'v' prefix from helm tags ([8f2557e](https://github.com/MyElectricalData/myelectricaldata_new/commit/8f2557e4d2816f668e4c24211d61e2d9ca73e73c))
26+
* **ci:** trigger release only on apps changes ([dfd98af](https://github.com/MyElectricalData/myelectricaldata_new/commit/dfd98afaba9f3602e7827b84a52b392003852f7b))
27+
* **ci:** use config swap instead of extends for helm release ([9eee893](https://github.com/MyElectricalData/myelectricaldata_new/commit/9eee893fce951e853915f204810f045796a46467))
28+
* **ci:** use helm/vX.X.X tags without GitHub releases ([9ab83ed](https://github.com/MyElectricalData/myelectricaldata_new/commit/9ab83eda8a0da3dcfd0d12a6af94b51f9ddab510))
29+
* **ci:** use semantic-release action for proper GitHub outputs ([4c5ead6](https://github.com/MyElectricalData/myelectricaldata_new/commit/4c5ead6f34f38f9ade6530a6e6d05e420c31b70d))
30+
* **helm:** correct postgres/valkey condition and configmap references ([61087da](https://github.com/MyElectricalData/myelectricaldata_new/commit/61087daf59425a7fe0d7ef7f928c6dc98cadc5b7))
31+
* **valkey:** update existingSecretKey to existingSecretPasswordKey in values.yaml and helpers.tpl ([9597eb4](https://github.com/MyElectricalData/myelectricaldata_new/commit/9597eb4e65947f6a18e6bdeb95f85b78126204cd))
32+
* **web:** handle nested arrays in offpeak_hours parsing ([609661b](https://github.com/MyElectricalData/myelectricaldata_new/commit/609661bdcd45913e37224b238264181a68594caf))
33+
* **web:** make login button full width on signup success ([07786f8](https://github.com/MyElectricalData/myelectricaldata_new/commit/07786f87493d7a508558fe529621f37cf45cdb5b))
34+
* **web:** sync OfferSelector state on page navigation ([2ec0cf1](https://github.com/MyElectricalData/myelectricaldata_new/commit/2ec0cf13ec462efa2e57a75d9f871a87f9d13ff0))
35+
36+
## [1.1.0-dev.7](https://github.com/MyElectricalData/myelectricaldata_new/compare/1.1.0-dev.6...1.1.0-dev.7) (2025-12-19)
37+
38+
### Bug Fixes
39+
40+
* **web:** sync OfferSelector state on page navigation ([2ec0cf1](https://github.com/MyElectricalData/myelectricaldata_new/commit/2ec0cf13ec462efa2e57a75d9f871a87f9d13ff0))
41+
42+
## [1.1.0-dev.6](https://github.com/MyElectricalData/myelectricaldata_new/compare/1.1.0-dev.5...1.1.0-dev.6) (2025-12-18)
43+
44+
### Bug Fixes
45+
46+
* **ci:** remove 'v' prefix from helm tags ([8f2557e](https://github.com/MyElectricalData/myelectricaldata_new/commit/8f2557e4d2816f668e4c24211d61e2d9ca73e73c))
47+
* **ci:** trigger release only on apps changes ([dfd98af](https://github.com/MyElectricalData/myelectricaldata_new/commit/dfd98afaba9f3602e7827b84a52b392003852f7b))
48+
* **ci:** use config swap instead of extends for helm release ([9eee893](https://github.com/MyElectricalData/myelectricaldata_new/commit/9eee893fce951e853915f204810f045796a46467))
49+
* **ci:** use helm/vX.X.X tags without GitHub releases ([9ab83ed](https://github.com/MyElectricalData/myelectricaldata_new/commit/9ab83eda8a0da3dcfd0d12a6af94b51f9ddab510))
50+
* **valkey:** update existingSecretKey to existingSecretPasswordKey in values.yaml and helpers.tpl ([9597eb4](https://github.com/MyElectricalData/myelectricaldata_new/commit/9597eb4e65947f6a18e6bdeb95f85b78126204cd))
51+
* **web:** handle nested arrays in offpeak_hours parsing ([609661b](https://github.com/MyElectricalData/myelectricaldata_new/commit/609661bdcd45913e37224b238264181a68594caf))
852

953
## [1.1.0-dev.5](https://github.com/MyElectricalData/myelectricaldata_new/compare/1.1.0-dev.4...1.1.0-dev.5) (2025-12-18)
1054

apps/api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "myelectricaldata-api"
3-
version = "1.1.0"
3+
version = "1.2.0-dev.2"
44
description = "MyElectricalData API Gateway for Enedis data"
55
authors = [{name = "m4dm4rtig4n"}]
66
license = {text = "Apache-2.0"}

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "myelectricaldata-web",
3-
"version": "1.1.0",
3+
"version": "1.2.0-dev.2",
44
"type": "module",
55
"scripts": {
66
"dev": "vite",

apps/web/src/components/OfferSelector.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -95,32 +95,25 @@ export default function OfferSelector({
9595
return allOffers.find(o => o.id === selectedOfferId) || null
9696
}, [selectedOfferId, allOffers])
9797

98-
// Initialize/sync selectors from selected offer
99-
// This effect handles two cases:
100-
// 1. Initial mount with a selectedOfferId - wait for offers to load then sync
101-
// 2. selectedOfferId prop changes externally - sync to new offer
102-
// We track which offer we've synced to avoid interfering with manual user selection
103-
const [lastSyncedOfferId, setLastSyncedOfferId] = useState<string | null | undefined>(undefined)
104-
98+
// Sync selectors when selectedOffer changes (after data is loaded)
99+
// This handles:
100+
// 1. Initial mount - wait for offers to load, then sync
101+
// 2. Page navigation (back to Dashboard) - offers reload, sync when ready
102+
// 3. External offer change - sync immediately
105103
useEffect(() => {
106104
if (selectedOffer) {
107-
// We have a valid offer - sync the selectors if not already synced to this offer
108-
if (lastSyncedOfferId !== selectedOffer.id) {
109-
setSelectedProviderId(selectedOffer.provider_id)
110-
setSelectedOfferType(selectedOffer.offer_type)
111-
setLastSyncedOfferId(selectedOffer.id)
112-
}
105+
// Always sync when selectedOffer is available
106+
// This ensures selectors are populated after page navigation
107+
setSelectedProviderId(selectedOffer.provider_id)
108+
setSelectedOfferType(selectedOffer.offer_type)
113109
} else if (selectedOfferId === null || selectedOfferId === undefined) {
114-
// Offer was cleared externally - reset selectors
115-
if (lastSyncedOfferId !== null) {
116-
setSelectedProviderId(null)
117-
setSelectedOfferType(null)
118-
setLastSyncedOfferId(null)
119-
}
110+
// Offer was cleared - reset selectors
111+
setSelectedProviderId(null)
112+
setSelectedOfferType(null)
120113
}
121-
// Note: when selectedOfferId is set but selectedOffer is null (offers loading),
122-
// we wait - the effect will re-run when offers finish loading
123-
}, [selectedOffer, selectedOfferId, lastSyncedOfferId])
114+
// When selectedOfferId is set but offers not loaded yet, wait
115+
// The effect re-runs when allOffers populates and selectedOffer becomes non-null
116+
}, [selectedOffer, selectedOfferId])
124117

125118
// Reset offer type when provider changes
126119
const handleProviderChange = (providerId: string | null) => {

apps/web/src/components/PDLCard.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,17 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
9090
if (!pdl.offpeak_hours) return [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }]
9191

9292
if (Array.isArray(pdl.offpeak_hours)) {
93-
return pdl.offpeak_hours.length > 0
94-
? pdl.offpeak_hours.flatMap(parseAllRanges)
93+
// Filter to only strings and parse
94+
const stringValues = pdl.offpeak_hours.filter((v): v is string => typeof v === 'string')
95+
return stringValues.length > 0
96+
? stringValues.flatMap(parseAllRanges)
9597
: [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }]
9698
}
9799

98100
// Legacy format: convert object to array and deduplicate
99-
const values = Object.values(pdl.offpeak_hours).filter(Boolean) as string[]
101+
// Handle nested arrays (e.g., {"ranges": ["22:00-06:00"]})
102+
const rawValues = Object.values(pdl.offpeak_hours).filter(Boolean)
103+
const values = rawValues.flatMap(v => Array.isArray(v) ? v : [v]).filter((v): v is string => typeof v === 'string')
100104
const uniqueValues = Array.from(new Set(values))
101105
return uniqueValues.length > 0
102106
? uniqueValues.flatMap(parseAllRanges)
@@ -147,11 +151,15 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
147151
if (!pdl.offpeak_hours) {
148152
setOffpeakRanges([{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }])
149153
} else if (Array.isArray(pdl.offpeak_hours)) {
150-
const parsed = pdl.offpeak_hours.flatMap(parseAllRanges)
154+
// Filter to only strings and parse
155+
const stringValues = pdl.offpeak_hours.filter((v): v is string => typeof v === 'string')
156+
const parsed = stringValues.flatMap(parseAllRanges)
151157
setOffpeakRanges(parsed.length > 0 ? parsed : [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }])
152158
} else {
153159
// Legacy format: convert object to array and deduplicate
154-
const values = Object.values(pdl.offpeak_hours).filter(Boolean) as string[]
160+
// Handle nested arrays (e.g., {"ranges": ["22:00-06:00"]})
161+
const rawValues = Object.values(pdl.offpeak_hours).filter(Boolean)
162+
const values = rawValues.flatMap(v => Array.isArray(v) ? v : [v]).filter((v): v is string => typeof v === 'string')
155163
const uniqueValues = Array.from(new Set(values))
156164
const parsed = uniqueValues.flatMap(parseAllRanges)
157165
setOffpeakRanges(parsed.length > 0 ? parsed : [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }])

apps/web/src/pages/Signup.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState, useEffect, useRef, useMemo } from 'react'
22
import { Link } from 'react-router-dom'
33
import { useAuth } from '@/hooks/useAuth'
4-
import { Copy, Check, Eye, EyeOff, AlertCircle, UserPlus } from 'lucide-react'
4+
import { Copy, Check, Eye, EyeOff, AlertCircle, UserPlus, Download } from 'lucide-react'
55
import { logger } from '@/utils/logger'
66

77
declare global {
@@ -52,6 +52,7 @@ export default function Signup() {
5252
const [turnstileError, setTurnstileError] = useState(false)
5353
const [credentials, setCredentials] = useState<{ client_id: string; client_secret: string } | null>(null)
5454
const [copied, setCopied] = useState<'id' | 'secret' | null>(null)
55+
const [showSecret, setShowSecret] = useState(false)
5556
const { signup, signupLoading, signupError } = useAuth()
5657

5758
const TURNSTILE_SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY
@@ -143,6 +144,23 @@ export default function Signup() {
143144
setTimeout(() => setCopied(null), 2000)
144145
}
145146

147+
const downloadCredentials = () => {
148+
if (!credentials) return
149+
const data = {
150+
client_id: credentials.client_id,
151+
client_secret: credentials.client_secret,
152+
}
153+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
154+
const url = URL.createObjectURL(blob)
155+
const a = document.createElement('a')
156+
a.href = url
157+
a.download = 'myelectricaldata-credentials.json'
158+
document.body.appendChild(a)
159+
a.click()
160+
document.body.removeChild(a)
161+
URL.revokeObjectURL(url)
162+
}
163+
146164
if (credentials) {
147165
return (
148166
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
@@ -161,7 +179,7 @@ export default function Signup() {
161179
<div className="space-y-4">
162180
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
163181
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium">
164-
⚠️ Important : Sauvegardez ces identifiants maintenant. Ils ne seront plus affichés.
182+
⚠️ Important : Sauvegardez ces identifiants maintenant. Le Client Secret ne sera plus affichable, mais vous pourrez en générer un nouveau depuis votre compte.
165183
</p>
166184
</div>
167185

@@ -188,11 +206,18 @@ export default function Signup() {
188206
<label className="block text-sm font-medium mb-2 text-gray-900 dark:text-white">Client Secret</label>
189207
<div className="flex gap-2">
190208
<input
191-
type="text"
209+
type={showSecret ? 'text' : 'password'}
192210
value={credentials.client_secret}
193211
readOnly
194212
className="input flex-1"
195213
/>
214+
<button
215+
onClick={() => setShowSecret(!showSecret)}
216+
className="btn btn-secondary"
217+
title={showSecret ? 'Masquer' : 'Afficher'}
218+
>
219+
{showSecret ? <EyeOff size={20} /> : <Eye size={20} />}
220+
</button>
196221
<button
197222
onClick={() => copyToClipboard(credentials.client_secret, 'secret')}
198223
className="btn btn-secondary"
@@ -202,10 +227,18 @@ export default function Signup() {
202227
</button>
203228
</div>
204229
</div>
230+
231+
<button
232+
onClick={downloadCredentials}
233+
className="btn btn-secondary w-full flex items-center justify-center gap-2"
234+
>
235+
<Download size={20} />
236+
Télécharger en JSON
237+
</button>
205238
</div>
206239

207240
<div className="mt-8">
208-
<Link to="/login" className="btn btn-primary w-full">
241+
<Link to="/login" className="btn btn-primary w-full block text-center">
209242
Continuer vers la connexion
210243
</Link>
211244
</div>

apps/web/src/utils/offpeakHours.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,16 @@ export function parseOffpeakHours(offpeakConfig?: string[] | Record<string, stri
1515
if (!offpeakConfig) return []
1616

1717
const ranges: OffpeakRange[] = []
18-
const rangeStrings: string[] = Array.isArray(offpeakConfig)
19-
? offpeakConfig.filter((item): item is string => typeof item === 'string')
20-
: Object.values(offpeakConfig).filter((item): item is string => typeof item === 'string' && Boolean(item))
18+
19+
// Handle array format and object format
20+
// Also handle nested arrays like {"ranges": ["22:00-06:00"]}
21+
let rawValues: unknown[]
22+
if (Array.isArray(offpeakConfig)) {
23+
rawValues = offpeakConfig
24+
} else {
25+
rawValues = Object.values(offpeakConfig).flatMap(v => Array.isArray(v) ? v : [v])
26+
}
27+
const rangeStrings: string[] = rawValues.filter((item): item is string => typeof item === 'string' && Boolean(item))
2128

2229
for (const range of rangeStrings) {
2330
// Skip if range is not a string (safety check)

0 commit comments

Comments
 (0)