Compare commits

..

163 Commits

Author SHA1 Message Date
Henrik Jess Nielsen
6d0ba453b0 Nomad stuff
All checks were successful
Build and Deploy LifeFAQ / build-image (push) Successful in 51s
2025-10-05 16:13:39 +02:00
Henrik Jess Nielsen
ae902c51f4 Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 34s
2025-10-05 16:12:09 +02:00
Henrik Jess Nielsen
4d3f640ecd Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 2m2s
2025-10-05 16:07:20 +02:00
Henrik Jess Nielsen
7f87efbeb7 Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 3m10s
2025-10-05 16:01:55 +02:00
Henrik Jess Nielsen
f318440572 Nomad stuff
Some checks failed
Build and Deploy LifeFAQ / build-image (push) Failing after 3m21s
2025-10-05 15:54:42 +02:00
Henrik Jess Nielsen
0747579dcf Nomad stuff
Some checks are pending
Build, Push, and Deploy to Nomad / docker-nomad (push) Waiting to run
2025-10-05 15:53:10 +02:00
Henrik Jess Nielsen
6e97805eea Nomad stuff
Some checks are pending
Build, Push, and Deploy to Nomad / docker-nomad (push) Waiting to run
2025-10-05 15:48:30 +02:00
Henrik Jess
57739f565a [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m10s
2025-02-10 14:28:05 +01:00
Henrik Jess
b5521765a1 [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 2m18s
2025-02-04 18:08:35 +01:00
e4fd13a782 [main] Flight Things
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m54s
2025-01-21 21:04:40 +01:00
ae999e1fac [main] Wtf
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m22s
2025-01-20 01:24:08 +01:00
0176cf85e3 [main] sync
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m14s
2025-01-20 01:18:07 +01:00
907cf90b11 [main] sync
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m1s
2025-01-15 06:27:14 +01:00
Henrik Jess
e1207362de [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m10s
2025-01-14 19:51:29 +01:00
Henrik Jess
5de92fa7a1 [main] Prod Creds
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 2m8s
2025-01-14 17:25:45 +01:00
0129164fd5 [main] Fucking api fuck lort
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m1s
2025-01-13 23:42:16 +01:00
75c7a76210 [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 58s
2025-01-07 22:47:50 +01:00
805ff80ce8 [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m3s
2025-01-07 22:43:30 +01:00
2b9361c7e9 [main] Content..
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2025-01-07 22:42:46 +01:00
8573e330d6 [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2025-01-06 21:11:48 +01:00
d1364558ee [main] Content..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 40s
2025-01-05 22:48:09 +01:00
efa5d28d1d Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2025-01-04 03:21:19 +01:00
1f1948e40d Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy with Blue/Green or Canary / build-push (push) Successful in 16s
Build, Push, and Deploy with Blue/Green or Canary / blue-green-deploy (push) Failing after 0s
Build, Push, and Deploy with Blue/Green or Canary / canary-deploy (push) Failing after 0s
2025-01-04 03:17:08 +01:00
0580e5121e Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy with Blue/Green or Canary / build-push (push) Successful in 16s
Build, Push, and Deploy with Blue/Green or Canary / blue-green-deploy (push) Failing after 0s
Build, Push, and Deploy with Blue/Green or Canary / canary-deploy (push) Failing after 0s
2025-01-04 03:13:26 +01:00
f1d1b8ea93 Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2025-01-04 03:02:40 +01:00
2e878e24f6 Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy to Nomad with Waypoint / docker-waypoint-nomad (push) Failing after 18s
2025-01-04 03:01:28 +01:00
2643f19669 [main] Rollback..
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 46s
2025-01-04 02:14:26 +01:00
957ded280a [main] Actions test
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 23s
2025-01-04 02:11:20 +01:00
d701fc3adf [main] Actions test
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 23s
2025-01-04 02:09:54 +01:00
df07f530f6 [main] Actions test
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2025-01-04 02:08:52 +01:00
09686129b9 [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2025-01-04 02:07:11 +01:00
1cd4e7d4ee [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 44s
2025-01-04 02:06:14 +01:00
f8b216bea3 [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2025-01-03 09:52:21 +01:00
9f7e431126 [main] Lidt mere context
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 44s
2025-01-02 22:57:54 +01:00
08846aa70f [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2025-01-02 21:45:32 +01:00
3125f68b66 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2025-01-02 21:44:11 +01:00
269d623ca9 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 48s
2024-12-31 01:15:08 +01:00
ca86177e90 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 50s
2024-12-31 00:54:44 +01:00
a126778f16 [main] More styling, and some content in job
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 51s
2024-12-31 00:43:04 +01:00
1b3d03cd70 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 51s
2024-12-31 00:05:36 +01:00
c329891e2e [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 55s
2024-12-30 23:29:04 +01:00
57dde1df71 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2024-12-30 23:20:54 +01:00
15bda3d3d5 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 48s
2024-12-30 22:43:08 +01:00
5ad5e527a7 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 46s
2024-12-30 22:12:12 +01:00
7f08417eb1 [main] Images images images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 51s
2024-12-30 21:37:36 +01:00
13bc417d45 [main] Image webp
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 47s
2024-12-30 20:47:04 +01:00
bf1401c32b [main] Imagesizes 150x150
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 52s
2024-12-29 19:34:12 +01:00
2ab66fc0a3 [main] Imagesizes 150x150 2024-12-29 19:26:26 +01:00
0416201742 [main] Imagesizes 150x150
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 45s
2024-12-29 18:47:43 +01:00
10de61cb25 [main] Imagesizes in slider
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 53s
2024-12-29 18:12:24 +01:00
e5960232f1 [main] requirements
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 46s
2024-12-29 10:59:18 +01:00
6c24ac7ec2 [main] requirements
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 1m1s
2024-12-29 04:34:54 +01:00
0914787be6 [main] Playing with images
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-29 04:34:25 +01:00
0121662530 Lets make the frontpage in markdown too
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-29 04:34:15 +01:00
95b6c1fa05 Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 40s
2024-12-24 01:41:46 +01:00
668a1ae7a3 Lets make the frontpage in markdown too 2024-12-24 01:41:29 +01:00
71a89b7b10 Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 33s
2024-12-24 01:38:41 +01:00
1ff4ae2b24 Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 38s
2024-12-24 01:34:48 +01:00
677867dbdd [main] Playing with images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 39s
2024-12-24 01:12:20 +01:00
4059d6d7be Lets make the frontpage in markdown too
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
2024-12-24 00:50:24 +01:00
6d7f365069 [main] Playing with images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 36s
2024-12-24 00:30:04 +01:00
8e7f86a78b [main] Playing with images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
2024-12-24 00:28:06 +01:00
a5b560e404 [main] Playing with images
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 9s
2024-12-24 00:26:41 +01:00
87512d2e6d [main] Playing with images
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 10s
2024-12-24 00:14:44 +01:00
87efffe1c1 Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-22 00:09:07 +01:00
42f765366d Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
2024-12-22 00:04:56 +01:00
e392b43123 Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 35s
2024-12-21 23:56:46 +01:00
79eeb1b10c Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 39s
2024-12-21 23:47:48 +01:00
5b6f1d22b4 Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 39s
2024-12-21 23:41:56 +01:00
904ca07f09 Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 47s
2024-12-21 23:34:02 +01:00
bde441326b Lets see what lighthouse says
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-21 23:33:49 +01:00
3026972dee Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-21 22:13:04 +01:00
5dcdbd38fb Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 36s
2024-12-21 21:19:12 +01:00
e3ef48c788 Lets see what lighthouse says
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-21 21:19:01 +01:00
cf60aacb0f Lets see what lighthouse says
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 40s
2024-12-21 20:55:41 +01:00
b80c192eba Update templates/base_template.html
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 40s
2024-12-21 17:58:52 +01:00
79b087b99c lang
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 33s
2024-12-21 03:10:40 +01:00
fdad51dc6e lang
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-21 03:10:27 +01:00
cb6b674522 lang
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 33s
2024-12-21 03:04:14 +01:00
323adc9d28 [main] Playing with images
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 41s
2024-12-21 03:00:29 +01:00
d240c06364 lang
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-21 02:54:32 +01:00
71d338d6fc lang
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 39s
2024-12-21 02:51:27 +01:00
7c9dc1cd01 næste side fix
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 36s
2024-12-21 02:47:31 +01:00
162591f527 næste side fix
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-21 02:27:07 +01:00
4c33ada9b2 [main] Score fixes
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
2024-12-21 02:16:29 +01:00
9a3039b011 næste side fix
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 43s
2024-12-21 02:02:35 +01:00
a1c2233358 næste side fix
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 38s
2024-12-21 01:29:42 +01:00
494eb6c156 Meta tags
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 41s
2024-12-21 01:04:00 +01:00
c69dca47e1 Meta tags
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 39s
2024-12-21 00:55:55 +01:00
8dacced1b4 update gitignore
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-21 00:45:27 +01:00
306e45da5f update gitignore
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 38s
2024-12-21 00:17:13 +01:00
a6bc0707fb update gitignore
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-21 00:16:44 +01:00
bd2fef9b6c Lidt CSS fixeri og ja BS4 til at fixe tags
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-21 00:15:58 +01:00
7d55191d91 cache fix og title
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-20 23:20:57 +01:00
6beef9fc96 cache fix og title
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
2024-12-20 23:20:49 +01:00
a549f3f7a6 cache fix og title
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 41s
2024-12-20 23:15:44 +01:00
0e82979a70 Middelware update and footer content
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 43s
2024-12-20 22:40:44 +01:00
c8ad950d98 Middelware update and footer content
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 42s
2024-12-20 22:39:00 +01:00
d58e5b0d12 Middelware update and footer content
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 39s
2024-12-20 22:35:39 +01:00
18181d7ce8 Better CSS
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 33s
2024-12-20 20:11:28 +01:00
Henrik Jess
74685601b2 Lets restore
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 38s
2024-12-20 10:44:33 +01:00
Henrik Jess
240ee1afbf Lets restore
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 7s
2024-12-20 10:43:35 +01:00
Henrik Jess
abf69e0074 Lets restore
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 7s
2024-12-20 10:43:24 +01:00
Henrik Jess
e502ea69aa Blue Green
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 7s
2024-12-20 10:42:35 +01:00
Henrik Jess
0b9eb0b9f0 Blue Green
Some checks failed
Build, Push, and Blue/Green Deploy to Nomad / docker-nomad (push) Failing after 6s
2024-12-20 10:40:06 +01:00
Henrik Jess
23a22fb4d6 Blue Green
Some checks failed
Build, Push, and Blue/Green Deploy to Nomad / docker-nomad (push) Failing after 6s
2024-12-20 10:37:56 +01:00
Henrik Jess
1b047a74fd Blue Green
Some checks failed
Build, Push, and Blue/Green Deploy to Nomad / docker-nomad (push) Failing after 7s
2024-12-20 10:35:54 +01:00
Henrik Jess
459f51808f Blue Green
Some checks failed
Build, Push, and Blue/Green Deploy to Nomad / docker-nomad (push) Failing after 38s
2024-12-20 10:11:25 +01:00
Henrik Jess
0b66a49d07 Blue Green
Some checks failed
Build, Push, and Blue/Green Deploy to Nomad / docker-nomad (push) Failing after 38s
2024-12-20 10:02:56 +01:00
Henrik Jess
bc4dc6b5fb Blue Green
Some checks failed
Build, Push, and Blue/Green Deploy to Nomad / docker-nomad (push) Failing after 38s
2024-12-20 10:01:37 +01:00
Henrik Jess
7e0d7e6466 Blue Green
Some checks failed
Build, Push, and Blue/Green Deploy to Nomad / docker-nomad (push) Failing after 38s
2024-12-20 09:57:40 +01:00
Henrik Jess
8aeb6dbd24 requirements.txt
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 57s
2024-12-20 09:43:25 +01:00
Henrik Jess
7e5c31b00d Bug somewhere
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 40s
2024-12-19 20:20:07 +01:00
Henrik Jess
af9c8f77fd Bug somewhere
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 35s
2024-12-19 15:57:20 +01:00
Henrik Jess
47d0834c12 Bug somewhere
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 39s
2024-12-19 15:41:41 +01:00
b78fb42ed7 Merge pull request 'Sync' (#13) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 38s
Reviewed-on: #13
2024-12-19 15:37:47 +01:00
Henrik Jess
33ca681f10 Bug somewhere
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 33s
2024-12-18 21:53:43 +01:00
Henrik Jess
f32172879c Bug somewhere
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 30s
2024-12-18 21:50:04 +01:00
Henrik Jess
f80c529a46 Bug somewhere
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-18 21:39:19 +01:00
5d6b621a99 Better CSS
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 36s
2024-12-17 22:34:05 +01:00
8d9f714701 Better CSS
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
2024-12-17 22:03:47 +01:00
b4c673bdc4 Better CSS 2024-12-17 21:28:10 +01:00
dea59f3d23 Sync
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 37s
2024-12-17 19:06:34 +01:00
Henrik Jess
7072e7e099 Bug somewhere
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 4m38s
2024-12-17 17:10:37 +01:00
Henrik Jess
1cb9e066ab Modals
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 33s
2024-12-17 14:40:26 +01:00
4e43c10b54 Sync
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 30s
2024-12-16 23:15:37 +01:00
87257a0bd4 Sync 2024-12-16 21:13:50 +01:00
Henrik Jess
4009d49ee6 https enforcement
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 32s
2024-12-16 17:04:15 +01:00
Henrik Jess
f13aa9ec7e https enforcement 2024-12-16 17:04:06 +01:00
ce74aa5113 Merge pull request 'mvc' (#12) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
Reviewed-on: #12
2024-12-13 23:18:39 +01:00
48ba5feff9 Lets rememeber those pictures 2024-12-13 23:12:00 +01:00
3416dea62e Lets go back 2024-12-13 23:11:30 +01:00
eadd90322e Lets go back 2024-12-13 22:35:06 +01:00
11e66000c9 Merge pull request 'Lets go back' (#11) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 32s
Reviewed-on: #11
2024-12-13 22:20:38 +01:00
7a6cf87e3e Lets go back 2024-12-13 22:19:08 +01:00
afd60d3d58 Merge pull request 'Trying something ...' (#10) from mvc into main
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
Reviewed-on: #10
2024-12-13 22:16:16 +01:00
70df58a097 Trying something ... 2024-12-13 22:14:44 +01:00
8b57e2af8b Merge pull request 'Trying something ...' (#9) from mvc into main
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
Reviewed-on: #9
2024-12-13 22:13:25 +01:00
87b10611e7 Trying something ... 2024-12-13 22:12:03 +01:00
2029db143f Merge pull request 'Trying something ...' (#8) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 8s
Reviewed-on: #8
2024-12-13 22:09:22 +01:00
f4570ca7cc Trying something ... 2024-12-13 22:07:43 +01:00
93c8a066cb Merge pull request 'Renaming + CSS' (#7) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 32s
Reviewed-on: #7
2024-12-13 22:01:49 +01:00
4efe32116f Renaming + CSS 2024-12-13 22:00:13 +01:00
a2c6b94da1 Merge pull request 'mvc' (#6) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 14s
Reviewed-on: #6
2024-12-13 21:55:54 +01:00
f18f82fb99 Renaming + CSS 2024-12-13 21:52:50 +01:00
5875cc785b Renaming 2024-12-13 20:39:13 +01:00
a259de8adb Renaming 2024-12-13 20:37:54 +01:00
fce888183d Renaming 2024-12-13 20:35:33 +01:00
04acb1b5d9 Renaming 2024-12-13 20:34:46 +01:00
0d0b1c57b3 Budget 2024-12-13 20:07:04 +01:00
61bbdf421f Budget 2024-12-13 19:14:16 +01:00
63372e8210 Merge pull request 'Loads and loads of data' (#5) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
Reviewed-on: #5
2024-12-12 23:32:37 +01:00
c751d0b072 Loads and loads of data 2024-12-12 23:30:19 +01:00
e2e8c8bf66 Merge pull request 'Clas based startup' (#4) from mvc into main
All checks were successful
Build, Push, and Deploy to Nomad / docker-nomad (push) Successful in 34s
Reviewed-on: #4
2024-12-12 20:07:41 +01:00
46a1951586 Clas based startup 2024-12-12 20:05:47 +01:00
adfa478eca Merge pull request 'Lets test' (#3) from mvc into main
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 4m30s
Reviewed-on: #3
2024-12-12 20:02:38 +01:00
146670d7c7 Lets test 2024-12-12 20:01:18 +01:00
7f7dd5139e Merge pull request 'mvc' (#2) from mvc into main
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Has been cancelled
Reviewed-on: #2
2024-12-12 20:00:00 +01:00
5d2bce8d6e [mvc] requirements 2024-12-12 19:58:26 +01:00
c6ac4599f4 Lets test 2024-12-12 19:55:30 +01:00
ffa1ae346f Merge pull request 'mvc' (#1) from mvc into main
Some checks failed
Build, Push, and Deploy to Nomad / docker-nomad (push) Failing after 4m28s
Reviewed-on: #1
2024-12-11 23:59:51 +01:00
e4887345a5 Lets test 2024-12-11 23:56:30 +01:00
82228fdb27 Lets test 2024-12-11 23:56:15 +01:00
468 changed files with 44992 additions and 430 deletions

View File

@@ -1,63 +1,150 @@
name: Build, Push, and Deploy to Nomad
name: Build and Deploy LifeFAQ
on:
push:
branches:
- main
workflow_dispatch:
jobs:
docker-nomad:
runs-on: self-hosted
build-image:
runs-on: debian-host
env:
PATH: /usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/sbin:/bin:/snap/bin
DOCKER_HOST: unix:///var/run/docker.sock
BUILDX_CONFIG: /tmp/buildx
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to Container Registry
run: echo ${{ secrets.password }} | docker login registry.i80.dk -u ${{ secrets.username }} --password-stdin
- name: System info
run: |
uname -a
whoami
- name: Build Docker Image
run: |
COMMIT_HASH=$(git rev-parse --short HEAD)
docker build -t registry.i80.dk/gitea/lifefaq:latest -t registry.i80.dk/gitea/lifefaq:${COMMIT_HASH} .
- name: Set up Docker Context for Buildx
id: buildx-context
run: |
export DOCKER_HOST=tcp://docker:2376/
export DOCKER_TLS_VERIFY=0
docker context rm builders || true
docker context create builders
- name: Verify Docker
run: docker --version
- name: Push Docker Image
run: |
COMMIT_HASH=$(git rev-parse --short HEAD)
echo "registry.i80.dk/gitea/lifefaq:latest"
echo "registry.i80.dk/gitea/lifefaq:${COMMIT_HASH}"
docker push registry.i80.dk/gitea/lifefaq:${COMMIT_HASH}
docker push registry.i80.dk/gitea/lifefaq:latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
env:
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
- name: Log in to Docker Registry
run: |
echo "${{ secrets.HARBOR_ROBOT_TOKEN }}" | docker login registry.i80.dk -u "robot\$gitserver" --password-stdin
env:
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
- name: Validate Nomad Job
env:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job validate .gitea/workflows/nomad-job.hcl
- name: Check for changes
id: changes
uses: dorny/paths-filter@v2
with:
filters: |
docker:
- 'Dockerfile'
- 'app/**'
- 'requirements.txt'
- name: Stop old deployment
env:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job stop -purge -no-shutdown-delay lifefaq
continue-on-error: true
- name: Build and push Docker image
if: steps.changes.outputs.docker == 'true'
uses: docker/build-push-action@v5
env:
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
with:
context: .
file: ./Dockerfile
push: true
tags: |
registry.i80.dk/gitea/lifefaq:latest
- name: Test container health
run: |
echo "=== Starting container for health check ==="
- name: Apply Nomad Job
env:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job run .gitea/workflows/nomad-job.hcl
docker pull registry.i80.dk/gitea/lifefaq:latest
- name: Update Nginx Configuration
run: ssh runner@nomad sudo /opt/nginx_updater/venv/bin/python3 /opt/nginx_updater/nginx_updater.py lifefaq
CONTAINER_ID=$(docker run -d \
-p 8000:8000 \
-e PORT=8000 \
-e APP_ENV=production \
--name lifefaq-test \
registry.i80.dk/gitea/lifefaq:latest)
- name: Update Forwarder Configuration
run: ssh runner@nomad sudo /opt/nginx_updater/venv/bin/python3 /opt/nginx_updater/update_forwarder.py --subdomain lifefaq
echo "Container started: ${CONTAINER_ID}"
echo "Waiting for /health endpoint..."
SUCCESS=false
for i in {1..90}; do
if curl -f -s http://localhost:8000/health > /dev/null 2>&1; then
echo "✓ Health check passed after ${i} seconds"
curl -s http://localhost:8000/health | jq '.' || echo "Health endpoint returned OK"
SUCCESS=true
break
fi
echo "Attempt ${i}/90 - waiting..."
sleep 1
done
# - name: Restart Nomad Job
# env:
# NOMAD_ADDR: https://nomad.i80.dk
# run: |
# nomad job stop lifefaq
# sleep 5 # Optional: Wait to ensure the old allocation is stopped
# nomad job run .gitea/workflows/nomad-job.hcl
echo "=== Container Logs ==="
docker logs lifefaq-test
docker stop lifefaq-test
docker rm lifefaq-test
if [ "$SUCCESS" = false ]; then
echo "✗ Health check failed after 90 seconds"
exit 1
fi
echo "✓ Container health check passed - safe to deploy"
env:
PATH: /usr/bin:/usr/local/bin:/bin:/sbin:/usr/sbin
- name: Deploy to Nomad
run: |
nomad job validate lifefaq.nomad
nomad job run lifefaq.nomad
env:
NOMAD_ADDR: "https://nomad.i80.dk:4646"
- name: Wait for deployment
run: |
echo "Checking deployment status..."
nomad job status lifefaq
echo "=== Allocation Details ==="
nomad job allocs lifefaq
echo "=== Getting logs from allocations ==="
for alloc in $(nomad job allocs -all lifefaq | tail -n +2 | awk '{print $1}'); do
echo "Logs for allocation $alloc:"
timeout=250
SECONDS=0
until nomad alloc logs "$alloc" 2>/dev/null || [ $SECONDS -gt $timeout ]; do
echo "Waiting for allocation to start... ($SECONDS/$timeout seconds)"
sleep 5
done
[ $SECONDS -gt $timeout ] && echo "Timeout for $alloc"
echo "---"
done
env:
NOMAD_ADDR: "https://nomad.i80.dk:4646"
- name: Notify deployment status
run: |
echo "✅ Deployment completed!"
echo "LifeFAQ should be available at: https://lifefaq.i80.dk"
echo "Health check endpoint: https://lifefaq.i80.dk/health"

1
.gitignore vendored
View File

@@ -3,4 +3,5 @@
.ídea
.idea/*
.gitea/**/build*/
data/**/index.html

View File

@@ -35,11 +35,11 @@ jobs:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job validate .gitea/workflows/nomad-job.hcl
- name: Stop old deployment
env:
NOMAD_ADDR: https://nomad.i80.dk
run: nomad job stop -purge -no-shutdown-delay [[PROJECT_NAME]]
continue-on-error: true
# - name: Stop old deployment
# env:
# NOMAD_ADDR: https://nomad.i80.dk
# run: nomad job stop -purge -no-shutdown-delay [[PROJECT_NAME]]
# continue-on-error: true
- name: Apply Nomad Job

View File

@@ -1,4 +1,4 @@
job "lifefaq" {
job "lifefaq-blue" {
region = "global"
datacenters = ["dc1"]
type = "service"
@@ -9,7 +9,7 @@ job "lifefaq" {
progress_deadline = "6m"
}
group "lifefaq-group" {
group "lifefaq-blue-group" {
count = 1
network {
@@ -21,7 +21,7 @@ job "lifefaq" {
# Register the service with Consul
service {
provider = "consul"
name = "lifefaq"
name = "lifefaq-blue"
port = "port-app"
# Traefik-specific tags for routing
@@ -38,7 +38,7 @@ job "lifefaq" {
}
}
task "lifefaq-task" {
task "lifefaq-blue-task" {
driver = "docker"
config {
@@ -58,3 +58,4 @@ job "lifefaq" {
}
}
}

View File

@@ -0,0 +1,59 @@
job "lifefaq-blue" {
region = "global"
datacenters = ["dc1"]
type = "service"
update {
stagger = "60s"
max_parallel = 1
canary = 1
auto_revert = true
auto_promote = true
progress_deadline = "6m"
}
group "lifefaq-group" {
count = 1
network {
port "port-app" {
to = 9210
}
}
service {
provider = "consul"
name = "lifefaq"
port = "port-app"
tags = [
"blue",
"PORT=${NOMAD_PORT_port-app}"
]
check {
name = "tcp_check"
type = "tcp"
interval = "10s"
timeout = "2s"
}
}
task "lifefaq-task" {
driver = "docker"
config {
image = "registry.i80.dk/gitea/lifefaq:blue"
ports = ["port-app"]
}
env {
APP_ENV = "production"
PORT = "${NOMAD_PORT_port-app}"
}
resources {
cpu = 250
memory = 80
}
}
}
}

View File

@@ -0,0 +1,59 @@
job "lifefaq-canary" {
region = "global"
datacenters = ["dc1"]
type = "service"
update {
stagger = "60s"
max_parallel = 1
canary = 1
auto_revert = true
auto_promote = true
progress_deadline = "6m"
}
group "lifefaq-group" {
count = 1
network {
port "port-app" {
to = 9210
}
}
service {
provider = "consul"
name = "lifefaq"
port = "port-app"
tags = [
"canary",
"PORT=${NOMAD_PORT_port-app}"
]
check {
name = "tcp_check"
type = "tcp"
interval = "10s"
timeout = "2s"
}
}
task "lifefaq-task" {
driver = "docker"
config {
image = "registry.i80.dk/gitea/lifefaq:${COMMIT_HASH}"
ports = ["port-app"]
}
env {
APP_ENV = "production"
PORT = "${NOMAD_PORT_port-app}"
}
resources {
cpu = 250
memory = 80
}
}
}
}

View File

@@ -0,0 +1,59 @@
job "lifefaq-green" {
region = "global"
datacenters = ["dc1"]
type = "service"
update {
stagger = "60s"
max_parallel = 1
canary = 1
auto_revert = true
auto_promote = true
progress_deadline = "6m"
}
group "lifefaq-group" {
count = 1
network {
port "port-app" {
to = 9210
}
}
service {
provider = "consul"
name = "lifefaq"
port = "port-app"
tags = [
"green",
"PORT=${NOMAD_PORT_port-app}"
]
check {
name = "tcp_check"
type = "tcp"
interval = "10s"
timeout = "2s"
}
}
task "lifefaq-task" {
driver = "docker"
config {
image = "registry.i80.dk/gitea/lifefaq:green"
ports = ["port-app"]
}
env {
APP_ENV = "production"
PORT = "${NOMAD_PORT_port-app}"
}
resources {
cpu = 250
memory = 80
}
}
}
}

81
Depriced/waypoint.hcl Normal file
View File

@@ -0,0 +1,81 @@
project = "lifefaq"
app "lifefaq" {
build {
use "docker" {
image = "registry.i80.dk/gitea/lifefaq:latest"
}
}
deploy {
use "nomad" {
job = <<EOT
job "lifefaq" {
region = "global"
datacenters = ["dc1"]
type = "service"
update {
stagger = "60s"
max_parallel = 1
canary = 1
auto_revert = true
auto_promote = true
progress_deadline = "6m"
}
group "lifefaq-group" {
count = 1
network {
port "port-app" {
to = 9210 # Internal application port
}
}
service {
provider = "consul"
name = "lifefaq"
port = "port-app"
tags = [
"PORT=${NOMAD_PORT_port-app}"
]
check {
name = "tcp_check"
type = "tcp"
interval = "10s"
timeout = "2s"
}
}
task "lifefaq-task" {
driver = "docker"
config {
image = "registry.i80.dk/gitea/lifefaq:latest"
ports = ["port-app"]
}
env {
APP_ENV = "production"
PORT = "${NOMAD_PORT_port-app}"
}
resources {
cpu = 250
memory = 80
}
}
}
}
EOT
}
}
release {
use "nomad" {
strategy = "bluegreen"
}
}

View File

@@ -1,20 +1,13 @@
# Base image with Python 3.11
FROM python:3.11-slim
# Set the working directory in the container
WORKDIR /app
# Copy the requirements file to the working directory
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Expose the port the FastAPI app runs on (default Uvicorn port)
EXPOSE 9210
# Port will be set via environment variable
EXPOSE 8000
# Command to run the FastAPI application
CMD ["uvicorn", "app:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "9210", "--workers", "1"]
CMD ["sh", "-c", "uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port ${PORT:-8000} --workers 1"]

BIN
FlyverPriser.ods Normal file

Binary file not shown.

47
Makefile Normal file
View File

@@ -0,0 +1,47 @@
.PHONY := help install run start stop docker-build docker-rebuild docker-run docker-stop docker-logs docker-shell clean
PROJECT_NAME ?= lifefaq
IMAGE_NAME ?= $(PROJECT_NAME):latest
CONTAINER_NAME ?= $(PROJECT_NAME)-app
PYTHON ?= python3
UVICORN ?= uvicorn
APP_MODULE ?= app.main:app
PORT ?= 8000
HOST_PORT ?= $(PORT)
help:
@printf "Available targets:\n"
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "} {printf " %-18s %s\n", $$1, $$2}' | sort
install: ## Install Python dependencies
$(PYTHON) -m pip install -r requirements.txt
run: ## Run the FastAPI app using app.py (no auto-reload)
$(PYTHON) app.py
start: ## Start the FastAPI app with uvicorn auto-reload (foreground)
$(UVICORN) $(APP_MODULE) --reload --host 0.0.0.0 --port $(PORT)
stop: ## Stop local uvicorn processes started via make start (best effort)
-pkill -f "$(UVICORN).*$(APP_MODULE)"
docker-build: ## Build the Docker image
docker build -t $(IMAGE_NAME) .
docker-rebuild: ## Rebuild the Docker image without cache
docker build --no-cache -t $(IMAGE_NAME) .
docker-run: ## Run the Docker container in the background
docker run --rm -d -p $(HOST_PORT):$(PORT) --name $(CONTAINER_NAME) -e PORT=$(PORT) $(IMAGE_NAME)
docker-stop: ## Stop the running Docker container
-docker stop $(CONTAINER_NAME)
docker-logs: ## Tail logs from the Docker container
docker logs -f $(CONTAINER_NAME)
docker-shell: ## Open a shell inside the running Docker container
docker exec -it $(CONTAINER_NAME) /bin/sh
clean: ## Remove Python cache artifacts
find . -name '__pycache__' -type d -prune -exec rm -rf {} +

BIN
PortugalBudget.ods Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

108
app.py
View File

@@ -1,100 +1,12 @@
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
from contextlib import asynccontextmanager
import json
import os
from markdown_render import render_markdown_with_jinja
import uvicorn
from app.main import app
class App:
def __init__(self):
"""Initialize the FastAPI app."""
self.app = FastAPI(lifespan=self.lifespan)
self.templates = Jinja2Templates(directory="templates")
self.data = self.load_mock_data()
# Mount directories
self.app.mount("/data", StaticFiles(directory="data"), name="data")
self.app.mount("/static", StaticFiles(directory="static"), name="static")
# Add routes
self.add_routes()
# 1. Lifespan events
@asynccontextmanager
async def lifespan(self, app: FastAPI):
print("App startup: Processing Markdown files...")
self.process_markdown_files("./data", "./data") # Process all Markdown files
print("Markdown processing complete!")
yield # Allow the app to start
print("App shutdown: Cleanup complete.")
# 2. Load JSON data
def load_mock_data(self):
"""Load mock data from a JSON file."""
with open("mock_data.json") as file:
return json.load(file)
# 3. Markdown processing logic
@staticmethod
def process_markdown_files(input_dir: str, output_dir: str):
"""
Recursively process all Markdown files in the input directory,
render them to HTML, and save them in the output directory.
"""
for root, _, files in os.walk(input_dir):
for file in files:
if file.endswith(".md"):
input_file_path = os.path.join(root, file)
relative_path = os.path.relpath(input_file_path, input_dir)
output_file_path = os.path.join(output_dir, os.path.splitext(relative_path)[0] + ".html")
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
with open(input_file_path, "r", encoding="utf-8") as md_file:
markdown_content = md_file.read()
print(f"Processing: {input_file_path} -> {output_file_path}")
rendered_html = render_markdown_with_jinja(markdown_content)
with open(output_file_path, "w", encoding="utf-8") as html_file:
html_file.write(rendered_html)
# 4. Routes
def add_routes(self):
"""Add all routes to the FastAPI app."""
@self.app.get("/", response_class=HTMLResponse)
async def get_index(request: Request):
"""Index route."""
return self.templates.TemplateResponse(
"index.html",
{"request": request, "data": self.data, "page_title": "Forside", "author": "Henrik"},
)
@self.app.get("/category/{category_name}", response_class=HTMLResponse)
async def get_category(request: Request, category_name: str):
"""Category route."""
category = next((cat for cat in self.data["categories"] if cat["path"] == category_name), None)
if category:
category_file = f"data/{category_name}/index.html"
if os.path.exists(category_file):
with open(category_file) as file:
category_content = file.read()
return self.templates.TemplateResponse(
"category.html",
{
"request": request,
"data": self.data,
"page_title": category["name"],
"author": category["author"],
"content": category_content,
},
)
return HTMLResponse("Kategori ikke fundet", status_code=404)
# Create the app instance
app_instance = App()
app = app_instance.app
if __name__ == "__main__":
port = int(os.getenv("PORT", 8000))
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=port,
reload=False
)

0
app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,123 @@
import os
import json
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
import time
from app.services.metadata_processor import MetadataProcessor
class CategoryController:
def __init__(self,data_file="generated_data.json"):
"""Initialize the controller."""
self.router = APIRouter()
self.templates = Jinja2Templates(directory="templates")
self.data = self._load_data( data_file )
self._add_routes()
def _add_routes(self):
"""Add routes to the router."""
self.router.add_api_route("/", self.get_index, methods=["GET"], response_class=HTMLResponse)
self.router.add_api_route(
"/category/{category_name}",
self.get_category,
methods=["GET"],
response_class=HTMLResponse,
)
self.router.add_api_route(
"/categories", self.list_categories, methods = ["GET"], response_class = JSONResponse
)
def _load_data(self, data_file):
"""Load JSON data from a file. If the file is missing, generate it."""
if not os.path.exists(data_file):
print(f"{data_file} not found. Generating JSON...")
self.generate_json() # Call the JSON generation method
with open(data_file, "r", encoding="utf-8") as file:
return json.load(file)
async def get_index(self, request: Request):
"""
Handle requests for the index (home) page.
This function is executed every time the root route (index page) is accessed.
It renders the 'index.html' template and populates it with dynamic data, such as:
- 'page_title': A static title for the home page ("Forside").
- 'author': The author's name ("Henrik").
- 'data': General data accessible to the template.
Args:
request (Request): The HTTP request object.
Returns:
TemplateResponse: A rendered HTML page for the index (home) route.
"""
unix_time_now = int( time.time() )
with open(f"data/_frontpage/index.html", "r") as fp:
content = fp.read()
return self.templates.TemplateResponse(
"category.html",
{
"request": request,
"data": self.data,
"page_title": "Frontpage",
"author": "Henrik Jess",
"content": content,
"timestamp": unix_time_now
})
async def get_category(self, request: Request, category_name: str):
"""
Handle requests for specific category pages.
This function is executed every time a category route is accessed.
It dynamically retrieves and serves content for the requested category.
- Searches for the requested category in 'self.data["categories"]' based on the provided category name.
- Reads the 'index.html' file located under 'data/{category_name}/' if it exists.
- Returns the rendered 'category.html' template with the following dynamic data:
- 'page_title': The name of the category.
- 'author': The author of the category.
- 'content': The content of the 'index.html' file.
- 'timestamp': The current Unix time when the request is processed.
- Returns a 404 HTML response if the category is not found or the file does not exist.
Args:
request (Request): The HTTP request object.
category_name (str): The name of the category being accessed.
Returns:
TemplateResponse: A rendered HTML page with dynamic category content.
HTMLResponse: A 404 response if the category does not exist.
"""
category = next((cat for cat in self.data["categories"] if cat["path"] == category_name), None)
unix_time_now = int( time.time() )
if category:
category_file = f"data/{category_name}/index.html"
if os.path.exists(category_file):
with open(category_file) as file:
category_content = file.read()
return self.templates.TemplateResponse(
"category.html",
{
"request": request,
"data": self.data,
"page_title": category["name"],
"author": category["author"],
"content": category_content,
"timestamp": unix_time_now
},
)
return HTMLResponse("Kategori ikke fundet", status_code=404)
async def list_categories(self, request: Request):
"""Return a list of all categories with their name and path."""
categories = [
{ "name": category["name"], "path": category["path"] }
for category in self.data.get( "categories", [] )
]
return JSONResponse( content = categories )

View File

@@ -0,0 +1,64 @@
import os
import json
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse, Response
from fastapi.templating import Jinja2Templates
class DynamicController:
def __init__(self, data_dir: str):
"""Initialize the dynamic controller."""
self.router = APIRouter()
self.templates = Jinja2Templates(directory="templates")
self.data_dir = data_dir
self.data = self._load_mock_data()
self._add_dynamic_routes()
def _load_mock_data(self):
"""Load mock data from a JSON file."""
with open("generated_data.json") as file:
return json.load(file)
def _add_dynamic_routes(self):
"""Scan data directory and create dynamic routes."""
for root, dirs, files in os.walk(self.data_dir):
for directory in dirs:
route_path = f"/{directory}" # Create route based on directory name
directory_path = os.path.join(root, directory)
# Register route dynamically
self.router.add_api_route(
route_path,
self._serve_dynamic_template(directory, directory_path),
methods=["GET"],
response_class=HTMLResponse,
)
def _serve_dynamic_template(self, route_name: str, directory_path: str):
"""Closure to serve templates for each route."""
async def route_handler(request: Request):
# Look for index.html or render fallback content
index_html = os.path.join(directory_path, "index.html")
if os.path.exists(index_html):
with open(index_html, "r", encoding="utf-8") as file:
content = file.read()
# Find the author for this route from preloaded data
for category in self.data.get("categories", []):
if category["path"] == route_name:
author_name = category["author"]
break
# Pass required data to the template
return self.templates.TemplateResponse(
"category.html",
{
"request": request,
"page_title": route_name.capitalize(),
"content": content,
"author": author_name,
"data": self.data, # Pass additional data if needed
},
)
# Fallback: Return a 404 if no content is found
return Response(f"No content found for {route_name}", status_code=404)
return route_handler

View File

@@ -0,0 +1,86 @@
import json
import random
from fastapi import APIRouter, Request, FastAPI
from fastapi.templating import Jinja2Templates
from app.controllers.category_controller import CategoryController
class RouteToWeb:
def __init__(self, app: FastAPI):
"""Initialize the controller."""
self.router = APIRouter()
self.templates = Jinja2Templates(directory="templates")
self.app = app
self.category_controller = CategoryController()
self._add_routes()
self._add_global_middleware()
def _add_routes(self):
"""Add routes to the router."""
@self.router.get("/route-list", tags=["system"])
async def route_list(request: Request):
"""Render route list with categories."""
routes = [
{"path": route.path, "name": route.name or "Unnamed"}
for route in self.app.routes
]
categories = request.state.categories
return self.templates.TemplateResponse(
"route_list.html",
{"request": request, "routes": routes, "categories": categories, "page_title": "Route og Kategori Liste"},
)
def _add_global_middleware(self):
"""Middleware to add categories and next category globally to all requests."""
@self.app.middleware( "http" )
async def add_categories_to_request(request: Request, call_next):
def generate_dynamic_description(category_name: str) -> str:
"""Generate a dynamic and engaging link text for a category."""
templates = [
"Dyk ned i kategorien {category} og bliv inspireret!",
"Opdag alt, hvad du behøver at vide i kategorien {category}.",
"Udforsk {category}-kategorien og find noget nyt og spændende.",
"Lad dig fordybe i kategorien {category} der er meget at se!",
"Find din næste læseoplevelse i {category}-kategorien.",
"Gå på opdagelse i kategorien {category} og bliv klogere.",
"Der venter spændende indhold i {category}-kategorien klik her!",
"Vil du vide mere? Hele kategorien {category} er kun ét klik væk.",
"Læs videre i kategorien {category} og få ny inspiration.",
"Fordyb dig i {category}-kategorien og opdag nyt indhold.",
"Spring ind i {category}-kategorien og gå på opdagelse!",
"Find masser af viden og gode læseoplevelser i {category}-kategorien.",
"Udforsk hele kategorien {category} og bliv beriget med ny viden.",
"Der er mere at læse i {category}-kategorien gå ikke glip af det!",
"Tag et dybere kig i kategorien {category} og bliv inspireret!"
]
template = random.choice( templates )
return template.format( category = category_name.lower() )
"""Inject categories and next category into request.state globally."""
# Hent kategorier direkte fra CategoryController
categories_response = await self.category_controller.list_categories( request )
categories_data = categories_response.body.decode()
categories = json.loads( categories_data )
# Tilføj kategorier til request.state
request.state.categories = categories
# Find den aktuelle og næste kategori
current_path = request.url.path.split("/")[-1]
next_category = None
for index, category in enumerate( categories ):
if category["path"] == current_path:
# Find næste kategori (cirkulær, hvis det er den sidste)
next_index = (index + 1) % len( categories )
next_category = categories[next_index]
next_category["description"] = generate_dynamic_description( next_category["path"] )
break
# Tilføj næste kategori til request.state
request.state.next_category = next_category
response = await call_next( request )
return response

80
app/main.py Normal file
View File

@@ -0,0 +1,80 @@
from fastapi import FastAPI
from contextlib import asynccontextmanager
from fastapi.staticfiles import StaticFiles
import app
from app.controllers.route_to_web import RouteToWeb
from app.services.markdown_processor import MarkdownProcessor
from app.services.metadata_processor import MetadataProcessor
from app.controllers.category_controller import CategoryController
from fastapi.middleware.gzip import GZipMiddleware
from app.services.image_service import ImageService
class Application:
def __init__(self):
"""Initialize the FastAPI app and configure it."""
self.app = FastAPI( lifespan = self._lifespan_event )
self._set_image_sizes()
self._setup_static_files()
self._setup_health_route()
self._include_routers()
self._include_middelware()
@asynccontextmanager
async def _lifespan_event(self, app: FastAPI):
"""Lifespan event for startup and shutdown logic."""
print("App startup: Processing Markdown files...")
# Generate dynamic JSON data
metadata_processor = MetadataProcessor(input_dir="./data", output_file="generated_data.json",app=self.app)
metadata_processor.generate_json()
print("Generated dynamic data file.")
# Process Markdown files into HTML
processor = MarkdownProcessor(input_dir="./data", templates_dir="./templates",app=self.app)
processor.run()
yield
print("App shutdown: Cleanup complete.")
def _setup_static_files(self):
"""Mount static file directories."""
self.app.mount("/data", StaticFiles(directory="data"), name="data")
self.app.mount("/static", StaticFiles(directory="static"), name="static")
self.app.mount( "/images", StaticFiles( directory = "static/images" ), name = "images" )
def _include_routers(self):
"""Include all route controllers."""
category_controller = CategoryController()
image_service = ImageService(self.app)
route_to_web = RouteToWeb(self.app)
self.app.include_router( category_controller.router )
self.app.include_router(route_to_web.router)
self.app.include_router( image_service.router )
def _setup_health_route(self):
@self.app.get("/health", tags=["Health"])
async def health_check():
return {"status": "ok"}
def _include_middelware(self):
self.app.add_middleware( GZipMiddleware, minimum_size = 500 )
def _set_image_sizes(self):
self.app.state.IMAGE_SIZES = {
'thumbnails': {'width': 150, 'height': 150},
'large': {'width': 800, 'height': 600},
'small': {'width': 300, 'height': 300},
'original': {'width': None, 'height': None}, # Original størrelse
}
def get_app(self):
"""Return the FastAPI app instance."""
return self.app
application = Application()
app = application.get_app()

0
app/services/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,180 @@
import os
from pathlib import Path
from fastapi import HTTPException
from fastapi.responses import FileResponse
from fastapi import APIRouter, Request, FastAPI
from PIL import Image
class FileHandler:
def __init__(self, category=None, image_type=None, filename=None) -> Path:
self.filename = filename
self.category = category
self.image_type = image_type
@property
def src_file(self) -> str:
src_path = "data/{category}/images/{filename}"
return src_path.format( category = self.category, filename = self.filename )
@property
def dest_file(self) -> str:
base_url = "/images/{category}/{filename}"
return base_url.format( category = self.category, filename = self.filename )
@property
def dest_filename(self) -> str:
base_url = "static/images/{category}/{image_type}/{filename}"
return base_url.format( category = self.category, image_type = self.image_type, filename = self.filename )
@property
def dest_filename_webp(self) -> str:
base_url = "static/images/{category}/{image_type}/{filename}"
path = Path( base_url.format( category = self.category, image_type = self.image_type, filename = self.filename ) )
if path.suffix != ".webp":
path = path.with_suffix( ".webp" )
return str(path)
@property
def dest_path(self) -> str:
base_url = "static/images/{category}/{image_type}"
return base_url.format( category = self.category, image_type = self.image_type )
def __str__(self):
return (
f"FileHandler(\n"
f" filename='{self.filename}',\n"
f" category='{self.category}',\n"
f" image_type='{self.image_type}',\n"
f" src_file='{self.src_file}',\n"
f" dest_file='{self.dest_file}',\n"
f" dest_filename='{self.dest_filename}',\n"
f" dest_path='{self.dest_path}'\n"
f")"
)
def get_category(self, file_path):
# List all categories in the data directory
categories = [
name for name in os.listdir( "data/" )
if os.path.isdir( os.path.join( "data/", name ) )
]
# Search for the category in the file path
for category in categories:
if f"/{category}/" in file_path or f"\\{category}\\" in file_path:
return category
# Return None if no category matches
return None
class ImageService:
def __init__(self,app: FastAPI):
self.router = APIRouter()
self.app = app
self.IMAGE_SIZES = self.app.state.IMAGE_SIZES
#self._ensure_directories_exist()
self._add_routes()
def __str__(self):
"""
Provides a string representation of the class instance.
"""
base_paths_str = "\n".join(
[f"{key}: {value}" for key, value in self.base_paths.items()]
)
image_sizes_str = "\n".join(
[
f"{key}: width={value['width']}, height={value['height']}"
for key, value in self.image_sizes.items()
]
)
return f"<Class:ImageService Base Paths:{base_paths_str} Image Sizes:\n{image_sizes_str}"
def get_image_size(self, image_type: str) -> dict:
"""
Retrieve the width and height for a given image type from the app state.
Args:
request (Request): FastAPI request object.
image_type (str): The type of the image (e.g., 'thumbnails').
Returns:
dict: A dictionary with 'width' and 'height'.
"""
image_sizes = self.app.state.IMAGE_SIZES
if image_type not in image_sizes:
raise ValueError( f"Invalid image type: {image_type}. Must be one of {list( image_sizes.keys() )}" )
return image_sizes[image_type]
def _add_routes(self):
self.router.add_api_route(
"/image/{category}/{type}/{filename}",
self.get_image,
methods=["GET"],
response_class=FileResponse,
)
async def get_image(self, category: str, type: str, filename: str):
"""
Retrieve an image file from the specified category and type.
"""
file_path = self._resolve_path(category, type, filename)
return FileResponse(file_path)
def validate_image(self, file_path:FileHandler=None, width:int=None, height:int=None, overwrite = True ) -> bool:
if not os.path.exists( file_path.dest_filename ):
with Image.open( file_path.src_file ) as img:
print(file_path.src_file)
self._resize_image( img, file_path, width, height )
return True
with Image.open( file_path.dest_filename ) as img:
if img.width != width or img.height != height:
if overwrite:
self._resize_image( img, file_path, width, height )
return False
return True
def _resize_image(self, img: Image.Image, file_path: FileHandler, width: int, height: int):
resized_img = img.resize( (width, height), Image.Resampling.LANCZOS )
os.makedirs(file_path.dest_path,exist_ok = True)
resized_img.save( file_path.dest_filename )
resized_img.save( file_path.dest_filename_webp, format = "WEBP", quality = 90 ) # Adjust quality as needed
print(file_path.dest_filename_webp)
async def get_image(self, category: str, type: str, filename: str):
file_path = self._resolve_path( category, type, filename )
return FileResponse( file_path )
def image_tag(self, category: str, image_type: str, filename: str, alt: str = "", width: int = None,
height: int = None,css_class:str=None) -> str:
"""
Generate an HTML <img> tag with default sizes if dimensions are not provided.
"""
# Use default sizes if none are provided
default_size = self.get_image_size( image_type)
width = width or default_size.get( "width" )
height = height or default_size.get( "height" )
file_path = FileHandler(category = category,image_type = image_type,filename = filename)
p = Path(file_path.dest_path)
p.mkdir(parents = True, exist_ok = True)
self.validate_image( file_path, width = width,height=height, overwrite = True )
tag = f'<img src="/{file_path.dest_filename_webp}" alt="{alt}"'
# if width:
# tag += f' width="{width}"'
# if height:
# tag += f' height="{height}"'
if css_class:
tag += f' class="{css_class}"'
tag += ">"
return tag

View File

@@ -0,0 +1,95 @@
import os
from bs4 import BeautifulSoup
from fastapi import FastAPI
from app.services.markdown_render import MarkdownRenderer
from jinja2 import Environment, FileSystemLoader
class MarkdownProcessor:
"""
A class to process Markdown files, extract metadata, and generate a single
'index.html' per category directory using a custom rendering engine.
"""
def __init__(self, input_dir: str, templates_dir: str,app:FastAPI=None):
"""
Initialize the MarkdownProcessor.
Args:
input_dir (str): Root directory containing category subdirectories.
templates_dir (str): Directory containing Jinja2 templates.
"""
self.input_dir = input_dir
self.env = Environment(loader=FileSystemLoader(templates_dir))
self.app = app
def _process_markdown_files_in_directory(self, directory_path: str) -> list:
"""
Process all Markdown files in a directory using Markdown and Jinja2 custom tags.
Args:
directory_path (str): Path to the category directory.
Returns:
list: A list of processed sections containing metadata and rendered content.
"""
from pathlib import Path
sections = []
for file in sorted(os.listdir(directory_path)):
if file.endswith(".md"):
file_path = os.path.join(directory_path, file)
with open(file_path, "r", encoding="utf-8") as md_file:
markdown_content = md_file.read()
markdown_render = MarkdownRenderer(file_path=file_path,app=self.app)
# Process Markdown and Jinja2
rendered_content, metadata = markdown_render.render_markdown_with_jinja ( markdown_content )
# Append the section to the list
sections.append({
"name": metadata.get("title", "Untitled"),
"content": rendered_content,
"summary": metadata.get("summary", ""),
"author": metadata.get("author", "Unknown"),
})
return sections
def _generate_index_html(self, directory_path: str, sections: list, output_file: str):
"""
Generate the index.html file for a category using the combined sections.
Args:
directory_path (str): Path to the category directory.
sections (list): List of processed Markdown content and metadata.
output_file (str): Path to save the generated index.html.
"""
# Render the template with the combined sections
template = self.env.get_template("combined_template.html")
rendered_html = template.render(
title=os.path.basename(directory_path).capitalize(),
sections=sections
)
# Write the rendered HTML to index.html
os.makedirs(directory_path, exist_ok=True)
with open(output_file, "w", encoding="utf-8") as output:
soup = BeautifulSoup( rendered_html, 'html.parser' )
cleaned_html = soup.prettify(formatter="html5")
output.write(cleaned_html)
print(f"Generated: {output_file}")
def run(self):
"""
Run the Markdown processing workflow: one 'index.html' per category.
"""
for root, dirs, _ in os.walk(self.input_dir):
for directory in dirs:
category_path = os.path.join(root, directory)
output_file = os.path.join(category_path, "index.html")
# Process all Markdown files in the current category directory
sections = self._process_markdown_files_in_directory(category_path)
if sections:
self._generate_index_html(category_path, sections, output_file)

View File

@@ -0,0 +1,172 @@
from pathlib import Path
import sys
import markdown
from fastapi import FastAPI
from jinja2 import Environment, DictLoader
from markupsafe import Markup
from .image_service import ImageService, FileHandler
class MarkdownRenderer:
def __init__(self, file_path: str = None, app: FastAPI=None):
"""
Initialize the MarkdownRenderer with a Jinja2 environment and custom functions.
"""
self.app = app
self.image_service = ImageService(self.app)
self.jinja_env = self._create_jinja_environment()
self.file_path = file_path
def _create_jinja_environment(self) -> Environment:
"""
Create and configure the Jinja2 environment with custom functions.
Returns:
Environment: A configured Jinja2 environment.
"""
env = Environment(loader=DictLoader({"base_template": "{{ content | safe }}"}))
env.globals.update({
"img_left_overlay": self.img_left_overlay,
"box": self.box,
"note": self.note,
"warning": self.warning,
"link_to": self.link_to,
"slider": self.slider,
"image": self.get_image, # Add image handler function
})
return env
def img_left_overlay(self, src: str) -> str:
"""Render an image with overlay."""
return f'''
<div class="img-left-overlay">
<img src="{src}" alt="Overlay Image" loading="lazy">
<div class="overlay-text">Overlay Text</div>
</div>
'''
def box(self, title: str, content: str) -> str:
"""Render a box component."""
return f'''
<div class="box">
<strong>{title}</strong>
<p>{content}</p>
</div>
'''
def note(self, content: str) -> str:
"""Render a note component."""
return f'''
<div class="note">
<p>{content}</p>
</div>
'''
def link_to(self, title: str, url: str) -> str:
"""Render a link component."""
return f'''
<a href="{url}" target="_blank" rel="noopener noreferrer">{title}</a>
'''
def warning(self, content: str) -> str:
"""Render a warning component."""
return f'''
<div class="warning">
⚠️ <p>{content}</p>
</div>
'''
def slider(self, options: dict, images: list) -> str:
"""Render a slider component."""
import uuid
modal_id = uuid.uuid4().hex.upper()[0:6]
html_content = []
html_content.append('<div class="button-stack">')
for i, val in enumerate(images):
self.image_service = ImageService( self.app )
modal_id_current = f"{modal_id}_{i}"
modal_id_next = f"{modal_id}_{i + 1}" if i + 1 < len(images) else f"{modal_id}_0"
category = FileHandler().get_category(self.file_path)
thumbnal_img = self.image_service.image_tag(category = category, image_type = "thumbnails",filename = val,alt="A better description later on")
modal_img = self.image_service.image_tag(category = category, image_type = "large",filename = val,alt="A better description later on")
html_content.append(f"""
<button onclick="openModal('modal{modal_id_current}')" class="stacked-button">
{thumbnal_img}
</button>
<div class="modal" id="modal{modal_id_current}">
<div class="modal-content">
<h2>Modal {i}</h2>
{modal_img}
<div class="modal-buttons">
<button onclick="closeModal('modal{modal_id_current}')">Close</button>
<button class="next-btn" onclick="nextModal('modal{modal_id_current}', 'modal{modal_id_next}')">Next</button>
</div>
</div>
</div>
""")
html_content.append('</div>')
return '\n'.join(html_content)
def _get_category(self):
if isinstance(self.file_path, str):
this_path = Path(self.file_path)
return this_path.parent.name
return True
def get_image(self, image_type: str, filename: str, alt: str = "", width: int = None, height: int = None,css_class:str=None) -> Markup:
"""
Generate a dynamic HTML <img> tag for an image using ImageService's image_tag method.
"""
valid_types = ['thumbnails', 'large', 'small', 'original']
if image_type not in valid_types:
sys.tracebacklimit = 0
raise ValueError( f"Invalid image type: {image_type}. Must be one of {valid_types}" )
tag = self.image_service.image_tag(
category=self._get_category(),
image_type=image_type,
filename=filename,
alt=alt,
width=width,
height=height,
css_class=css_class
)
return Markup(tag)
def render_markdown_with_jinja(self, markdown_content: str):
"""
Convert Markdown to HTML and apply Jinja2 rendering for custom tags.
Args:
markdown_content (str): Raw Markdown content.
Returns:
tuple: Rendered HTML content and metadata as a dictionary.
"""
md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
intermediate_html = md.convert(markdown_content)
metadata = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
# Step 3: Pass the resulting HTML with Jinja2 custom tags through Jinja2
template = self.jinja_env.get_template("base_template")
final_html = template.render(content=intermediate_html)
# Step 4: Re-render final_html in Jinja2 for embedded tags like {{ image(...) }}
final_output = self.jinja_env.from_string(final_html).render()
return final_output, metadata

View File

@@ -0,0 +1,123 @@
import os
import markdown
import json
from typing import List, Dict
from fastapi import FastAPI
from app.services.image_service import ImageService, FileHandler
class MetadataProcessor:
"""
A class to scan Markdown files, extract front matter metadata,
and generate a structured JSON file.
"""
def __init__(self, input_dir: str, output_file: str,app:FastAPI=None):
"""
Initialize the MetadataProcessor.
Args:
input_dir (str): Directory containing Markdown files.
output_file (str): Path to save the generated JSON file.
"""
self.input_dir = input_dir
self.output_file = output_file
self.app = app
self.data = {"categories": [], "favorites": []}
def _extract_metadata(self, file_path: str) -> Dict:
"""
Extract front matter metadata using the 'markdown' package.
Args:
file_path (str): Path to the Markdown file.
Returns:
dict: A dictionary containing the extracted metadata.
"""
with open(file_path, "r", encoding="utf-8") as file:
markdown_content = file.read()
# Initialize Markdown with meta extension
md = markdown.Markdown(extensions=["extra", "nl2br", "meta"])
md.convert(markdown_content)
# Metadata is stored in md.Meta as a dictionary of lists
meta = {key: " ".join(value) for key, value in md.Meta.items()} if md.Meta else {}
return meta
def _process_directory(self):
"""
Recursively scan the input directory for Markdown files
and extract metadata to build the JSON structure.
"""
for root, _, files in os.walk(self.input_dir):
for file in files:
if file.endswith(".md"):
file_path = os.path.join(root, file)
metadata = self._extract_metadata(file_path)
if metadata:
# Add to 'categories'
self.data["categories"].append({
"name": metadata.get("name", "Unknown"),
"path": os.path.relpath(root, self.input_dir).replace(os.sep, "/"),
"author": metadata.get("author", "Unknown")
})
# Add to 'favorites' if 'favorite' is true
if metadata.get("favorite") and metadata["favorite"].lower() == "true":
image_type = "thumbnails"
category = os.path.relpath( root, self.input_dir ).replace( os.sep, "/" )
filehandler = FileHandler(category=category, image_type=image_type, filename=metadata.get("image"))
imageservice = ImageService(self.app)
default_size = imageservice.get_image_size( image_type )
width = default_size.get( "width" )
height = default_size.get( "height" )
image_tag = imageservice.image_tag(category, image_type, metadata.get("image","Unkown"))
print(filehandler.dest_filename_webp)
print(image_tag)
self.data["favorites"].append({
"name": metadata.get("name", "Unknown"),
"image": filehandler.dest_filename_webp,
"height": height,
"width": width,
"description": metadata.get("summary", "No description provided"),
"path": category,
})
def generate_json(self):
"""
Generate the JSON structure, deduplicate and sort categories by 'path',
then save it to the output file.
"""
self._process_directory() # Extract all markdown data into self.data
# Ensure 'categories' exists and is a list
if "categories" not in self.data:
self.data["categories"] = []
# Deduplicate 'categories' using 'path' as the unique key
unique_categories = { }
for category in self.data["categories"]:
if isinstance( category, dict ): # Ensure valid category structure
path = category.get( "path", "unknown" ) # Use 'path' as the unique key
if path not in unique_categories:
unique_categories[path] = category
# Replace the 'categories' list with a sorted version by 'path'
self.data["categories"] = sorted(
unique_categories.values(),
key = lambda x: x.get( "path", "unknown" )
)
# Save the updated JSON to file
with open( self.output_file, "w", encoding = "utf-8" ) as json_file:
json.dump( self.data, json_file, indent = 4, ensure_ascii = False )
print( f"Generated JSON saved to {self.output_file}" )
return True

View File

@@ -1,43 +0,0 @@
import os
from markdown_render import render_markdown_with_jinja
def process_markdown_files(input_dir: str, output_dir: str):
"""
Recursively process all Markdown files in the input directory,
render them to HTML, and save them to the output directory.
"""
for root, _, files in os.walk(input_dir):
for file in files:
if file.endswith(".md"):
input_file_path = os.path.join(root, file)
# Determine output file path (convert .md to .html)
relative_path = os.path.relpath(input_file_path, input_dir)
output_file_path = os.path.join(output_dir, os.path.splitext(relative_path)[0] + ".html")
# Ensure the output directory exists
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
# Read Markdown content
with open(input_file_path, "r", encoding="utf-8") as md_file:
markdown_content = md_file.read()
# Render Markdown with Jinja2
print(f"Processing: {input_file_path} -> {output_file_path}")
rendered_html = render_markdown_with_jinja(markdown_content)
# Write the rendered HTML to the output file
with open(output_file_path, "w", encoding="utf-8") as html_file:
html_file.write(rendered_html)
print("Markdown processing complete!")
if __name__ == "__main__":
# Input directory containing Markdown files
input_directory = "./data"
# Output directory where HTML files will be stored
output_directory = "./data"
# Start the processing
process_markdown_files(input_directory, output_directory)

BIN
data.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,27 @@
---
name: Arbejde i Portugal
description: Mine noter og fund om at arbejde i Portugal som EU-borger
author: Henrik Jess
date: ons 11 dec 22:16:13 CET 2024
summary: Praktisk info fra min research om arbejde i Portugal
favorite: false
image: images/pic09.jpg
category: Job
tags: [Portugal, Arbejde, EU-borgere, NIF-nummer, Socialsikring]
---
# Kan jeg som udlænding arbejde i Portugal?
Jeg er ikke selv flyttet til Portugal endnu, men jeg har brugt en del tid på at undersøge, hvad der kræves for at arbejde der som EU-borger. Her er mine noter, baseret på det, jeg har fundet på nettet, YouTube og forskellige guides.
Som dansker kan man heldigvis arbejde i Portugal uden at skulle søge om en arbejdstilladelse. Det skyldes EU's regler om fri bevægelighed, så på det punkt er det ret ligetil.
Men der er nogle praktiske ting, du skal have styr på, før du kan komme i gang:
- **NIF-nummer**: Det er et skattemæssigt identifikationsnummer, som du skal bruge til stort set alt i Portugal arbejde, bankkonto og bolig.
- **Socialsikringssystemet**: Du skal registrere dig i det portugisiske socialsikringssystem for at få adgang til sundhed og sociale ydelser.
Selvom processen virker enkel på papiret, går det igen i mange kilder, at det kan tage lidt tid, især hvis du ikke taler portugisisk. Flere anbefaler at få hjælp fra nogen, der har prøvet det før, eller bruge lokale rådgivere, hvis det bliver for bøvlet.
Jeg deler bare det, jeg har fundet indtil videre, så hvis du også overvejer at arbejde i Portugal, håber jeg, det kan give dig en god start.

View File

@@ -0,0 +1,77 @@
---
name: Arbejde i Portugal
description: Mine noter og fund om at arbejde i Portugal som EU-borger
author: Henrik Jess
date: ons 11 dec 22:16:13 CET 2024
summary: Men hvordan gør man det så?
favorite: false
image: images/pic09.jpg
category: Job
tags: [Portugal, Arbejde, EU-borgere, NIF-nummer, Socialsikring]
---
## De lidt praktiske ting
Men der er nogle praktiske ting, du skal have styr på, før du kan komme i gang:
### **1. Få et NIF-nummer**
NIF (Número de Identificação Fiscal) er dit portugisiske skatte-ID, og det er afgørende for at kunne arbejde, åbne en bankkonto eller leje en bolig.
{{ image('thumbnails', 'carto.jpg', alt='Mit fantatiske billed',css_class="image right") }}
**Sådan gør du:**
1. **Besøg et Finanças-kontor** (det portugisiske skattevæsen) i Portugal.
2. Medbring:
- Dit pas eller ID-kort.
- En midlertidig adresse i Portugal (det kan være en ven, et hotel eller en lejekontrakt).
- Hvis du endnu ikke har fast adresse i Portugal, skal du bruge en *fiscal representative* (en person eller et firma i Portugal, der repræsenterer dig skattemæssigt). Der findes mange tjenester, der kan hjælpe online.
3. Indsend ansøgningen, og du får normalt dit NIF med det samme.
---
#### **2. Registrér dig i det portugisiske socialsikringssystem**
For at kunne arbejde og få adgang til sundhedsydelser og sociale ydelser, skal du have et socialsikringsnummer (Número de Identificação da Segurança Social).
**Sådan gør du:**
1. Find en lokal *Segurança Social* (socialsikringskontor).
2. Medbring:
- Dit NIF-nummer.
- Din arbejds- eller ansættelseskontrakt (hvis du allerede har en).
- Dit pas eller ID-kort.
3. Indsend de nødvendige dokumenter. Hvis du er selvstændig, skal du udfylde en særlig formular for freelancere.
---
### **3. Åbn en portugisisk bankkonto**
En bankkonto er nødvendig for at få løn udbetalt.
**Sådan gør du:**
1. Vælg en bank, og book tid på en filial.
2. Medbring:
- Dit NIF-nummer.
- Dit pas eller ID-kort.
- Bevis på adresse (f.eks. en regning eller en lejekontrakt).
3. Nogle banker tilbyder også online-åbning, hvilket kan være hurtigere.
---
### **4. Meld dig til SEF (Udlændingemyndighederne)**
Hvis du planlægger at bo i Portugal i mere end tre måneder, skal du registrere dig hos SEF (Serviço de Estrangeiros e Fronteiras).
**Sådan gør du:**
1. Book tid online via SEF's hjemmeside.
2. Medbring:
- Dit NIF-nummer.
- Bevis på arbejde (ansættelseskontrakt eller anden dokumentation).
- Bevis på bopæl i Portugal.
3. Når registreringen er færdig, får du dit opholdsbevis.
---
### **Gode råd undervejs**
- **Sprog:** Mange portugisere taler engelsk, men det kan være en fordel at have en lokal guide eller tolk med, hvis du møder bureaukratiske udfordringer.
- **Hjælp:** Overvej at bruge tjenester som advokatfirmaer eller agenturer, der specialiserer sig i at hjælpe udlændinge med at komme i gang i Portugal. Det kan spare tid og frustration.
Jeg håber, denne trin-for-trin guide kan gøre det nemmere for dig at tage de første skridt. Det er en proces, der kræver lidt planlægning, men med tålmodighed er det helt klart muligt at komme godt i gang!
{{ box(title="Husk!", content="Jeg har ikke selv gennemgået denne proccess - så jeg har ikke fået den bekræftet endnu, den består rent af andres beretninger og online søgninger") }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -0,0 +1,47 @@
---
name: Populære områder i Portugal
description: En guide til attraktive områder for tilflyttere i Portugal
author: Henrik Jess
date: ons 11 dec 23:00:00 CET 2024
summary: Populære regioner som Lissabon, Porto og Algarve
favorite: false
image: images/pic05.jpg
category: Bolig
tags: [Portugal, Lissabon, Porto, Algarve, Coimbra, Viseu, Tilflyttere]
---
# Flytte til Portugal: Populære Områder og Overvejelser
## Populære Områder
Hvad jeg har googlet mig frem til Spænde byer jeg glæder mig til at besøge i Portugal
**{{ link_to(title="Lissabon", url="https://www.google.com/maps?q=Lissabon") }}** Som Portugals hovedstad byder Lissabon på en perfekt blanding af historie, moderne faciliteter og et pulserende kulturliv. Byen er et centrum for internationale virksomheder og tilbyder et rigt jobmarked, samtidig med at den imponerer med sine brostensbelagte gader og imponerende udsigter.
**{{ link_to(title="Porto", url="https://www.google.com/maps?q=Porto") }}** Porto er kendt for sin charme og verdensarvsbeskyttede bymidte. Byen har et blomstrende expat-fællesskab og er perfekt for dem, der søger en mere afslappet atmosfære med nem adgang til vinregionen Douro.
**{{ link_to(title="Algarve", url="https://www.google.com/maps?q=Algarve") }}** Dette solrige paradis er ideelt for dem, der værdsætter strande, golfbaner og en afslappet livsstil. Algarve er også kendt for at være et yndet sted for pensionister og dem, der ønsker en feriepræget tilværelse året rundt.
**{{ link_to(title="Braga", url="https://www.google.com/maps?q=Braga") }}** Braga er kendt som Portugals religiøse hovedstad og byder på smukke kirker, historiske monumenter og en ungdommelig vibe takket være et stort antal studerende. Byen er billigere end Lissabon og Porto, men stadig fuld af liv og aktiviteter.
**{{ link_to(title="Cascais", url="https://www.google.com/maps?q=Cascais") }}** For dem, der søger en luksuriøs livsstil tæt på Lissabon, er Cascais det perfekte valg. Denne kystby kombinerer en afslappet atmosfære med smukke strande, eksklusive boliger og en blomstrende expat-community.
**{{ link_to(title="Setúbal", url="https://www.google.com/maps?q=Setúbal") }}** Setúbal, syd for Lissabon, tilbyder smukke kyststrækninger og en rig historie. Byen er kendt for sin adgang til Arrábida-bjergene og nogle af landets bedste strande, alt sammen til en overkommelig pris.
## Alternativer til Storbyerne
For dem, der ønsker lavere boligpriser og en roligere hverdag, tilbyder Portugal mange alternativer til de travle storbyer:
**{{ link_to(title="Coimbra", url="https://www.google.com/maps?q=Coimbra") }}** Coimbra er en charmerende universitetsby, der kombinerer en rig historie med lavere leveomkostninger. Byen er ideel for dem, der ønsker at bo i en dynamisk, men mindre hektisk by.
**{{ link_to(title="Viseu", url="https://www.google.com/maps?q=Viseu") }}** Viseu er kendt for sin høje livskvalitet, smukke landskaber og prisvenlige boligmarked. Byen er perfekt for dem, der søger autentisk portugisisk kultur i en fredelig atmosfære.
**{{ link_to(title="Évora", url="https://www.google.com/maps?q=Évora") }}** Denne UNESCO-verdensarvsby i Alentejo-regionen er et fantastisk valg for dem, der elsker historie og tradition. Évora er kendt for sine velbevarede romerske ruiner og sin afslappede atmosfære.
**{{ link_to(title="Aveiro", url="https://www.google.com/maps?q=Aveiro") }}** Også kendt som "Portugals Venedig" byder Aveiro på charmerende kanaler, farverige både og en afslappet kystlivsstil. Byen er ideel for dem, der ønsker en kombination af historie og havudsigt.
**{{ link_to(title="Guimarães", url="https://www.google.com/maps?q=Guimarães") }}** Guimarães er kendt som "Portugal's fødested" og byder på en rig historie, fantastisk arkitektur og en hyggelig atmosfære. Det er en mindre, men kulturelt rig by, der tilbyder en autentisk portugisisk oplevelse.

View File

@@ -0,0 +1,23 @@
---
name: Boligsøgning i Portugal
description: Effektive platforme og tips til boligsøgning i Portugal
author: Henrik Jess
date: ons 11 dec 23:15:00 CET 2024
summary: Brug Idealista og tips til at finde bolig i Portugal
favorite: false
image: images/pic08.jpg
category: Bolig
tags: [Portugal, Bolig, Idealista, Boligsøgning, Flytning]
---
# Hvor finder man bolig?
En af de bedste platforme, jeg har fundet til boligsøgning i Portugal, er **{{ link_to(title="idealista.pt", url="https://www.idealista.pt/") }}**. Den minder lidt om DBA og er både overskuelig og nem at bruge. Det er hurtigt at oprette en profil, og tjenesten er tilmed gratis.
### Udfordringer ved boligsøgning
Hvis du ikke kender Portugal særlig godt, kan det være svært at beslutte, hvilket område der passer bedst til dine behov. Her er nogle tips:
- Brug kortfunktionen på Idealista til at få overblik over priser i forskellige områder.
- Overvej at besøge de mest interessante områder først, før du træffer din beslutning.
- Tjek lokale Facebook-grupper og fora for tips fra andre tilflyttere.
Portugal har meget at byde på, og det rigtige område afhænger af, om du søger storbyliv, strande eller roligere omgivelser.

View File

@@ -0,0 +1,40 @@
---
name: Bolig i Portugal
description: Købs- og lejepriser samt regionale forskelle i Portugal
author: Henrik Jess
date: ons 11 dec 22:45:00 CET 2024
summary: Boligpriser og ekstra omkostninger i Portugal
favorite: false
image: images/pic09.jpg
category: Bolig
tags: [Portugal, Bolig, Leje, Køb, Lissabon, Ejendomsskatter]
---
# Er det dyrt at købe eller leje bolig i Portugal?
Boligpriser i Portugal varierer meget afhængigt af regionen. I de større byer som **Lissabon** og **Porto** er priserne generelt høje, mens mindre byer som **Coimbra** og **Guarda** tilbyder mere overkommelige muligheder.
---
## Regionale forskelle
- **Lissabon og Porto**: Som de mest populære områder for både tilflyttere og turister har disse byer nogle af de højeste boligpriser i landet.
- **Mindre byer**: Byer som Coimbra, Viseu eller Guarda giver langt billigere alternativer og tilbyder en roligere livsstil.
---
## Ekstra omkostninger ved boligkøb
Ud over selve boligprisen skal man være opmærksom på flere ekstra udgifter:
- **Ejendomsskat (IMI)**: En årlig skat på mellem **0,3% og 0,45%** af boligens værdi.
- **IMT-afgift**: En engangsafgift ved køb af bolig, som afhænger af købsprisen.
- **Advokat- og notaromkostninger**: Ved boligkøb er det normalt at bruge advokat og notar for at sikre korrekt papirarbejde.
---
## Leje vs. køb
Hvis du ikke er klar til at købe, er **leje** en god mulighed. Lejepriser varierer også meget:
- I Lissabon og Porto er månedlig husleje typisk højere, men stadig billigere end i de fleste danske storbyer.
- I mindre byer kan du finde boliger til markant lavere priser, især hvis du er fleksibel med beliggenheden.
---
Boligmarkedet i Portugal byder på både muligheder og udfordringer. Mens de populære områder har højere priser, er der stadig gode alternativer i mindre byer. Når man medregner de lavere leveomkostninger i Portugal sammenlignet med Danmark, er der potentiale for at få mere værdi for pengene.

View File

@@ -0,0 +1,142 @@
---
name: Budget - Indkøb
description: En kort sammenligning af priserne
author: Henrik Jess
date: ons 11 dec 23:25:00 CET 2024
summary: Fødevarer er markant billigere i Portugal med få undtagelser som bær og specialvarer.
favorite: true
image: budget2.jpg
category: Økonomi
tags: [Portugal, Budget, Økonomi]
---
Jeg har taget udgangspunkt i et indkøb fra Mambeno / Rema1000 indkøb for en uge, og forsøgt og sammenligne det. Her er den fulde kvittering med korrekt **omregning til EUR**, de portugisiske priser og den **procentvise forskel** mellem Danmark og Portugal. Nogen af priserne er konsekvent højere, så er dt fordi der er købt store pakker, eksempelvis Laks.
---
{{ box(title="Disclaimer",content="Priserne er baseret på her-og-nu priser fra REMA 1000 i Danmark. Enkelte produkter kan have været på tilbud, hvilket kan påvirke sammenligningen.") }}
## **Samlet Oversigt**
**Valutakurs**: 1 EUR = 7,44 DKK
| **Nr.** | **Varebeskrivelse** | **Pris i Danmark (EUR)** | **Pris i Portugal (EUR)** | **Forskel (EUR)** | **% Forskel** |
|---------|--------------------------------|--------------------------|---------------------------|------------------:|--------------:|
| 1 | FLUTES | 0,87 | 0,50 | 0,37 | -42,5% |
| 2 | TANDPASTA COLGATE | 3,49 | 2,00 | 1,49 | -42,7% |
| 3 | SOLSIKKERUGBRØD DET GODE | 3,43 | 2,00 | 1,43 | -41,7% |
| 4 | TOILETPAPIR SOFT 3-LAGS | 2,75 | 1,80 | 0,95 | -34,5% |
| 5 | JORDBÆR (1 LTR) | 1,14 | 1,50 | -0,36 | +31,6% |
| 6 | SOLBÆR (1 LTR) | 1,14 | 1,50 | -0,36 | +31,6% |
| 7 | HINDBÆR/APPELSIN (1 LTR) | 1,14 | 1,40 | -0,26 | +22,8% |
| 8 | MINIMÆLK 0,4% | 1,61 | 0,90 | 0,71 | -44,1% |
| 9 | RISOTTO M/SVAMPE | 2,01 | 1,70 | 0,31 | -15,4% |
| 10 | KÆRGÅRDEN SMØRBAR LET | 3,49 | 2,00 | 1,49 | -42,7% |
| 11 | SDJ. SPEGEPØLSE 3-STJERNET | 2,68 | 2,00 | 0,68 | -25,4% |
| 12 | OKSE SPEGEPØLSE 3-STJERNET | 2,68 | 2,00 | 0,68 | -25,4% |
| 13 | KARTOFFELSPEGEPØLSE 3-STJERNET | 2,68 | 2,00 | 0,68 | -25,4% |
| 14 | KØDPØLSE 3-STJERNET | 2,41 | 2,00 | 0,41 | -17,1% |
| 15 | TORTILLAS HVEDE | 1,41 | 1,20 | 0,21 | -14,9% |
| 16 | GUACAMOLE DIP | 1,51 | 1,20 | 0,31 | -20,5% |
| 17 | SALSASAUCE HOT | 1,31 | 1,00 | 0,31 | -23,7% |
| 18 | SMØR ARLA, ØKOLOGISK | 3,62 | 2,50 | 1,12 | -31,0% |
| 19 | SKYR NATUREL | 2,41 | 2,00 | 0,41 | -17,1% |
| 20 | CHEDDAR OST | 2,01 | 1,80 | 0,21 | -10,4% |
| 21 | FRAICHE 5% | 2,68 | 2,20 | 0,48 | -17,9% |
| 22 | KRYDDEROST BUKO | 3,62 | 2,50 | 1,12 | -31,0% |
| 23 | FRILANDS BRUNCHÆG | 2,01 | 1,80 | 0,21 | -10,4% |
| 24 | TORTILLA CHIPS SALT | 1,04 | 1,00 | 0,04 | -3,8% |
| 25 | GROV CAFESANDWICH | 2,95 | 2,20 | 0,75 | -25,4% |
| 26 | FULDKORNSTORTILLAS | 1,65 | 1,40 | 0,25 | -15,2% |
| 27 | KYLLINGESCHNITZLER | 2,68 | 2,30 | 0,38 | -14,2% |
| 28 | RATATOUILLE | 1,88 | 1,50 | 0,38 | -20,2% |
| 29 | FINE ÆRTER | 1,61 | 1,20 | 0,41 | -25,5% |
| 30 | FAJITA BLANDING | 1,88 | 1,40 | 0,48 | -25,5% |
| 31 | SOLSIKKEKERNER | 1,55 | 1,20 | 0,35 | -22,6% |
| 32 | KOGEPOSE RIS | 1,01 | 0,90 | 0,11 | -10,9% |
| 33 | RASP | 0,93 | 0,80 | 0,13 | -14,0% |
| 34 | HASSELNØDDEKERNER | 1,61 | 1,20 | 0,41 | -25,5% |
| 35 | PENNE RIGATE | 1,14 | 1,00 | 0,14 | -12,3% |
| 36 | ROGN | 2,14 | 1,80 | 0,34 | -15,9% |
| 37 | TOMAT PASTA | 0,67 | 0,50 | 0,17 | -25,4% |
| 38 | SØD FRANSK SENNEP | 1,28 | 1,00 | 0,28 | -21,9% |
| 39 | SOJASAUCE | 1,34 | 1,10 | 0,24 | -17,9% |
| 40 | MAJS | 1,01 | 0,80 | 0,21 | -20,8% |
| 41 | HVIDE BØNNER I TOMAT | 0,93 | 0,80 | 0,13 | -14,0% |
| 42 | HAKKEDE TOMATER | 1,01 | 0,80 | 0,21 | -20,8% |
| 43 | GRØNTSAGSBOUILLON | 0,60 | 0,50 | 0,10 | -16,7% |
| 44 | KRYDDERURTEDRESSING | 1,68 | 1,20 | 0,48 | -28,6% |
| 45 | HONNING | 3,62 | 2,50 | 1,12 | -31,0% |
| 46 | SØNDERJYSK SPEGEPØLSE | 1,34 | 1,20 | 0,14 | -10,4% |
| 47 | LAKSEFILET | 10,75 | 7,00 | 3,75 | -34,9% |
| 48 | KYLLINGEBRYSTFILET | 8,06 | 5,50 | 2,56 | -31,8% |
| 49 | TOMATER I BAKKE | 1,61 | 1,20 | 0,41 | -25,5% |
| 50 | TØRRET TIMIAN | 0,67 | 0,50 | 0,17 | -25,4% |
| 51 | SPÆD KÅLSALAT | 3,36 | 2,50 | 0,86 | -25,6% |
| 52 | PERSILLE | 1,21 | 1,00 | 0,21 | -17,4% |
| 53 | ØKOLOGISKE CITRONER | 0,81 | 0,70 | 0,11 | -13,6% |
| 54 | RØD PEBER | 1,07 | 0,90 | 0,17 | -15,9% |
| 55 | AGURK | 0,81 | 0,70 | 0,11 | -13,6% |
---
### **Opsummering af Prissammenligning mellem Danmark og Portugal**
| **Parameter** | **Værdi** |
|--------------------------------------|--------------------------------|
| Antal varer sammenlignet | 55 |
| Gennemsnitlig pris i Danmark (EUR) | 2,42 |
| Gennemsnitlig pris i Portugal (EUR) | 1,84 |
| Gennemsnitlig besparelse | **24,0% lavere i Portugal** |
| Antal varer billigere i Portugal | 47 |
| Antal varer dyrere i Portugal | 8 |
1. **Generel besparelse**
Priserne i Portugal er **i gennemsnit 24,0% lavere** end i Danmark på tværs af de 55 varer. Dette er særligt tydeligt for basisvarer som smør, mælk og kolonialvarer.
2. **Flest varer er billigere i Portugal**
Af de 55 varer er **47 billigere** i Portugal, hvilket tydeligt afspejler de lavere leveomkostninger og en mere konkurrencedygtig fødevaresektor.
3. **Eksempler på store besparelser**
- **Kærgården Smørbar Let**: -42,7% billigere
- **Smør Arla Økologisk**: -31,0% billigere
- **Mælk (Minimælk)**: -44,1% billigere
- **Laksefilet**: -34,9% billigere
Disse basisvarer oplever en markant prisreduktion i Portugal, hvilket giver væsentlige besparelser i dagligdagen.
4. **Få varer dyrere i Portugal**
Der er **8 varer**, hvor priserne er højere i Portugal. Eksempler inkluderer:
- **Jordbær/Solbær**: +31,6% dyrere
- **Hindbær/Appelsin**: +22,8% dyrere
Disse prisforskelle kan tilskrives sæsonvariationer og højere importomkostninger. Det kan også begrundes i det kan være danske Tilbuds vare.
5. **Frisk frugt og grønt**
Generelt er grøntsager som **tomater, agurk og røde peberfrugter** betydeligt billigere i Portugal. Dog er enkelte varer som **bær** dyrere, sandsynligvis på grund af sæsonafhængighed og øgede importomkostninger.
---
Prissammenligningen viser, at dagligvarer generelt er **betydeligt billigere i Portugal**, især når det gælder smør, mælk og kolonialvarer, som typisk har besparelser på op til **50%**. Dette gør Portugal økonomisk attraktivt for husholdninger. Dog skal man være opmærksom på, at enkelte varer som **bær** og specialvarer kan være dyrere, hvilket er en vigtig faktor i planlægningen af leveomkostninger.
---
En anden (mere proff) side kommer til næsten de samme konklusioner, man kan måske godt se jeg har brugt Rema1000s tilbuds priser - Men de er relativt tæt på hinanden
- **Leveomkostninger i Portugal er 38,5% lavere end i Danmark (eksklusive husleje).**
- **Leveomkostninger inklusive husleje i Portugal er 32,4% lavere end i Danmark.**
- **Huslejepriser i Portugal er 13,1% lavere end i Danmark.**
- **Restaurantpriser i Portugal er 53,0% lavere end i Danmark.**
- **Dagligvarepriser i Portugal er 33,4% lavere end i Danmark.**
- **Den lokale købekraft i Portugal er 54,3% lavere end i Danmark.**
{{ link_to(title="Leveomkostningssammenligning mellem Danmark og Portugal - Numbeo", url="https://www.numbeo.com/cost-of-living/compare_countries_result.jsp?country1=Denmark&country2=Portugal") }} - De siger 33.4%
{{ link_to(title="Leveomkostninger mellem Danmark og Portugal - Livingcost", url="https://livingcost.org/cost/denmark/portugal") }} - De siger 28.3% som rammer også meget godt.

View File

@@ -0,0 +1,44 @@
---
name: Supermarkedssammenligning
description: En praktisk guide til at sammenligne supermarkeder i Danmark og Portugal.
author: Henrik Jess
date: ons 11 dec 23:55:00 CET 2024
summary: Find de bedste steder at handle i Portugal, sammenlignet med danske supermarkeder.
favorite: false
image: images/supermarked.jpg
category: Økonomi
tags: [Portugal, Indkøb, Supermarkeder, Økonomi]
---
### **Sammenligning af supermarkeder: Danmark vs Portugal**
| **Danmark** | **Portugal** | **Bemærkninger** |
|--------------------------|-------------------------|------------------------------------------------------|
| **REMA 1000** | **Pingo Doce** | Budgetvenligt supermarked med mange dagligvarer. |
| **Føtex** / **Bilka** | **Continente** | Stort supermarked med bredt udvalg og gode tilbud. |
| **Netto** | **Lidl** | Lavpris supermarked. Lidl findes i begge lande. |
| **SuperBrugsen** | **Auchan** (tidl. Jumbo)| Mid-range supermarked med fokus på kvalitet. |
| **Irma** | **El Corte Inglés** | Premium supermarked med internationale varer. |
| **Meny** | **Intermarché** | God kvalitet og et stort udvalg af varer. |
| **Aldi** | **Aldi** | Ensartet kæde i begge lande, kendt for lave priser. |
| **Kvickly** | **Minipreço** | Prisvenligt med mindre butikker i lokalområder. |
---
### **Anbefalede steder at handle i Portugal**
1. {{ link_to(title="**Pingo Doce**", url="https://www.pingodoce.pt") }} Kendt for lave priser og gode tilbud, især på dagligvarer som brød, mælk og grøntsager.
2. {{ link_to(title="**Continente**", url="https://www.continente.pt") }} Portugals svar på Bilka/Føtex med alt fra mad til husholdningsartikler. Ofte har de gode rabatprogrammer.
3. {{ link_to(title="**Lidl**", url="https://www.lidl.pt") }} Samme kendte koncept som i Danmark. Billige basisvarer og et mindre udvalg.
4. {{ link_to(title="**Auchan**", url="https://www.auchan.pt") }} (tidligere Jumbo) Større supermarkeder med et bredt udvalg af varer, inklusiv internationale produkter.
5. {{ link_to(title="**Intermarché**", url="https://www.intermarche.pt") }} Fokus på frisk frugt og grønt samt lokale portugisiske produkter.
6. {{ link_to(title="**Minipreço**", url="https://www.minipreco.pt") }} Lavprisalternativ med mindre butikker, som er nemme at finde i byområder.
7. {{ link_to(title="**El Corte Inglés**", url="https://www.elcorteingles.pt") }} Perfekt til luksusvarer og specialprodukter, men til lidt højere priser.
---
### **Tips til at spare penge i Portugal**
- **Frugt og grønt** er ofte billigst på lokale markeder, som fx **"Mercado Municipal"**.
- Brug **loyalitetskort** i supermarkeder som Pingo Doce og Continente for at få rabatter.
- Køb kød og fisk på **lokale slagtere** og fiskemarkeder for bedre priser og kvalitet.
- Planlæg ugentlige indkøb i større butikker som **Continente** for at udnytte kampagner og tilbud.

View File

@@ -0,0 +1,26 @@
---
name: El- og vandregninger i Portugal
description: Hvordan el- og vandregninger påvirker leveomkostningerne
author: Henrik Jess
date: ons 11 dec 23:20:00 CET 2024
summary: Elregninger er høje, mens vand og gebyrer er moderate
favorite: false
image: images/pic08.jpg
category: Leveomkostninger
tags: [Portugal, Leveomkostninger, Elregninger, Vandregninger, Gebyrer]
---
# Hvordan påvirker el- og vandregninger leveomkostningerne?
- **Elregninger**: Elektricitet i Portugal kan være relativt dyrt sammenlignet med andre europæiske lande. Det er en omkostning, der især mærkes i vintermånederne, hvor opvarmning kan trække prisen op.
- **Vand og affaldsgebyrer**: Disse regninger er typisk moderate og overkommelige, hvilket hjælper med at holde de samlede udgifter nede.
Elektricitetspriserne i Portugal er blandt de højeste i Europa. Ifølge Eurostat var den gennemsnitlige pris for husholdninger i første halvdel af 2024 €0,2426 per kWh, en stigning på 5,52% fra det foregående halvår. Denne stigning skyldes blandt andet landets afhængighed af importerede energikilder, som udgør omkring 65% af det samlede energiforbrug.
I modsætning hertil er omkostningerne til vand og affaldshåndtering i Portugal mere moderate. For en lejlighed på 85 m² ligger de månedlige udgifter til el, vand, varme, køling og affaldsbortskaffelse mellem €54 og €150, med et gennemsnit omkring midten af dette interval. Disse omkostninger er relativt lave sammenlignet med andre europæiske lande.
For at reducere de høje eludgifter, især i vintermånederne, kan det være fordelagtigt at investere i energieffektive løsninger og apparater. Desuden kan bevidsthed om energiforbrug og valg af tidspunkter med lavere elpriser bidrage til at minimere omkostningerne.
Sammenfattende er det vigtigt at tage højde for de relativt høje elpriser i Portugal, mens vand- og affaldsgebyrer forbliver moderate. Ved at implementere energieffektive tiltag kan man opnå en mere balanceret og overkommelig samlet leveomkostning.

View File

@@ -0,0 +1,94 @@
---
name: El- og vandregninger i Portugal: Sådan påvirker de leveomkostningerne
description: Undersøg hvordan elregninger og vandgebyrer i Portugal påvirker leveomkostningerne. Få indsigt i boligudgifter, dagligvarer og sammenligning med Danmark.
author: Henrik Jess
date: ons 11 dec 23:20:00 CET 2024
summary: Leveomkostningerne i Portugal er lave, især på bolig og dagligvarer. Få indsigt i, hvordan du kan leve godt og billigt under sydens sol med et gennemtænkt budget.
image: images/pic08.jpg
category: Økonomi
tags: ["Portugal", "Leveomkostninger", "Elregninger", "Vandregninger", "Gebyrer"]
---
Her er det reviderede **husholdningsbudget i Portugal** uden kontorfællesskab og flyrejser, så du kan sammenligne med leveomkostninger i Danmark.
---
## **Månedligt Budget i Portugal**
{{ box(title="Disclaimer",content="Det skal siges jeg har ikke de faktiske tal endnu, så det er 100% på tommelfinger vudering") }}
| **Kategori** | **Udgift pr. måned (EUR)** | **Bemærkninger** |
|---------------------------------|----------------------------|---------------------------------------|
| **Boligleje (100 m²)** | 1.000 - 1.200 | Afhænger af område (fx Lissabon dyrere) |
| **El og varme** | 120 - 150 | Højt i vintermånederne |
| **Vand og affaldsgebyrer** | 30 - 50 | Moderate priser |
| **Internet og mobil** | 40 - 60 | Kombipakke til hjem og telefoner |
| **Mad og dagligvarer** | 500 - 600 | Baseret på ugentlig kvittering (~125 EUR) |
| **Transport (offentlig/benzin)** | 50 - 100 | Offentlig transport og små ture |
| **Forsikringer** | 50 - 75 | Sundhed, indbo, bil osv. |
| **Sundhed** | 50 - 100 | Egenbetaling for tandlæge og medicin |
| **Fritid og aktiviteter** | 100 - 150 | Restauranter, biograf, hobbyer |
| **Erika's skoleudgifter** | 50 - 100 | Materialer og småudgifter |
|**Samlet månedlig udgift** | **1.990 - 2.585 EUR** | **Cirka 14.800 - 19.200 DKK** |
### **Kort analyse af budgetoversigten**
1. **Boligleje**: Boligleje udgør den største udgiftspost med **1.000 - 1.200 EUR** om måneden, afhængigt af området. Lissabon og større byer er typisk dyrere, mens mindre byer tilbyder mere overkommelige priser.
2. **Energiomkostninger (El og varme)**: El og varme koster mellem **120 - 150 EUR**, og dette kan stige i vintermånederne, især hvis opvarmning ikke er energieffektiv. Dette er en vigtig post at planlægge for.
3. **Vand og affaldsgebyrer**: Disse udgifter er relativt lave på **30 - 50 EUR**, hvilket hjælper med at holde de samlede faste omkostninger nede.
4. **Dagligvarer og mad**: Madbudgettet på **500 - 600 EUR** om måneden er realistisk for to personer og er baseret på en gennemsnitlig ugentlig udgift på **125 EUR**. Dette afspejler rimelige priser på dagligvarer i Portugal.
5. **Transport**: Transportomkostninger er beskedne på **50 - 100 EUR**, hvilket inkluderer offentlig transport og mindre biludgifter. Det lave niveau skyldes de lave priser på månedskort og brændstof i Portugal sammenlignet med Nordeuropa.
6. **Sundhed og forsikringer**: Sundhedsudgifter og forsikringer varierer mellem **50 - 100 EUR** hver. Dette inkluderer egenbetaling til tandlæge, medicin og basale forsikringer som sundhed og indbo.
7. **Fritid og aktiviteter**: Fritidsbudgettet på **100 - 150 EUR** dækker restauranter, biograf og hobbyer. Portugal tilbyder generelt billigere oplevelser, hvilket giver mere økonomisk frihed til fritidsaktiviteter.
8. **Skoleudgifter**: Erika's skoleudgifter er moderate med **50 - 100 EUR**, typisk til materialer og mindre udgifter. - Dog med tanke på folkeskole med int. sproglinje (ikke privat)
---
### **Samlet vurdering**
Det samlede budget på **1.990 - 2.585 EUR** om måneden (cirka **14.800 - 19.200 DKK**) viser, at det er muligt at leve komfortabelt i Portugal til en lavere pris end i Danmark.
- **Boligleje** er den største omkostning, men stadig overkommelig sammenlignet med danske storbypriser.
- **Mad og dagligvarer** udgør en betydelig, men rimelig del af budgettet.
- **Faste udgifter** som energi og vand er relativt lave, mens sundhed og fritidsaktiviteter giver økonomisk fleksibilitet.
Dette budget giver plads til en stabil og behagelig hverdag i Portugal uden store kompromiser på livskvalitet.
---
## **Sammenligning med Danmark**
| **Kategori** | **Portugal (EUR)** | **Danmark (EUR)** | **Besparelse (%)** |
|-----------------------------------|----------------------------|-------------------------|-------------------------|
| **Boligleje (100 m²)** | 1.000 - 1.200 | 1.500 - 2.000 | -33% til -50% |
| **El og varme** | 120 - 150 | 200 - 250 | -25% til -40% |
| **Vand og affaldsgebyrer** | 30 - 50 | 60 - 100 | -50% til -70% |
| **Internet og mobil** | 40 - 60 | 50 - 70 | -20% til -30% |
| **Mad og dagligvarer** | 500 - 600 | 800 - 1.000 | -25% til -40% |
| **Transport** | 50 - 100 | 150 - 200 | -50% til -66% |
| **Forsikringer** | 50 - 75 | 100 - 150 | -50% |
| **Sundhed** | 50 - 100 | 100 - 150 | -33% til -50% |
| **Fritid og aktiviteter** | 100 - 150 | 200 - 300 | -50% |
| **Samlet månedlig udgift** | **1.990 - 2.585** | **3.360 - 4.520** | **-30% til -45%** |
---
### **Analyse**
1. **Boligudgifter**
Bolig er markant billigere i Portugal med en besparelse på **33-50%** sammenlignet med Danmark. Lejepriserne i mindre byer er endnu lavere.
2. **Dagligvarer**
Indkøb af mad og andre dagligvarer koster i gennemsnit **25-40% mindre** i Portugal, som også reflekteres i din kvittering.
3. **El og varme**
Selvom elpriserne er høje i Portugal, er de stadig **25-40% lavere** end i Danmark, hvor opvarmningsomkostninger i vintermånederne er væsentligt højere.
4. **Transport og sundhed**
Offentlig transport og sundhedsydelser er også væsentligt billigere i Portugal med besparelser på op til **50-66%**.
5. **Samlede omkostninger**
Samlet set er leveomkostningerne i Portugal **30-45% lavere** end i Danmark. Dette giver et stort økonomisk råderum og mulighed for en bedre livskvalitet.
At bo i Portugal kan give betydelige besparelser på husleje, dagligvarer og generelle leveomkostninger. Selvom elpriserne kan være en udfordring, er det samlede billede væsentligt billigere end i Danmark, især med energieffektive løsninger.

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,22 @@
---
name: Permanent ophold i Portugal
description: Krav og praktiske trin for EU-borgere til permanent ophold
author: Henrik Jess
date: ons 11 dec 23:35:00 CET 2024
summary: Registrering, NIF-nummer og socialsikring i Portugal
favorite: false
image: images/pic07.jpg
category: Flytning
tags: [Portugal, Permanent ophold, EU-borger, NIF-nummer, Socialsikring, Flytning]
---
# Hvad kræves for at bo permanent i Portugal som dansk statsborger?
Som EU-borger er det relativt nemt at få permanent ophold i Portugal. Her er, hvad du skal have styr på:
1. **Registrering af adresse**: Du skal registrere dig som bosiddende i Portugal inden for de første 90 dage.
2. **NIF-nummer**: Det portugisiske skattemæssige identifikationsnummer er nødvendigt for stort set alle økonomiske aktiviteter.
3. **Socialsikringssystemet**: For at få adgang til sundhedssystemet og sociale ydelser skal du registrere dig i det portugisiske socialsikringssystem.
4. **Arbejdsforhold**: En ansættelseskontrakt eller bevis på selvstændig virksomhed er ofte nødvendigt for at dokumentere din økonomiske stabilitet.
Det er en proces, der virker overskuelig på papiret, men fra det jeg har læst og hørt, kan der være lidt bureaukrati involveret. Hvis du planlægger at flytte, er det en god idé at sætte tid af til at få styr på papirarbejdet, så alt glider så nemt som muligt.

View File

@@ -0,0 +1,24 @@
---
name: Åbning af bankkonto i Portugal
description: En guide til krav og fleksible løsninger for bankkonti
author: Henrik Jess
date: ons 11 dec 23:25:00 CET 2024
summary: Krav for bankkonti og fleksible løsninger som Wise
favorite: false
image: images/pic05.jpg
category: Bank og Økonomi
tags: [Portugal, Bankkonto, NIF-nummer, Wise, Økonomi]
---
# Hvordan åbner man en bankkonto i Portugal?
For at åbne en bankkonto i Portugal er der nogle få krav, du skal have styr på:
- **NIF-nummer**: Dit skattemæssige identifikationsnummer, som er nødvendigt for stort set alle økonomiske aktiviteter i Portugal.
- **Gyldig adresse**: Du skal kunne dokumentere din bopæl, enten i Portugal eller et andet land.
### Fleksible løsninger
Hvis du gerne vil undgå høje gebyrer og samtidig have adgang til lokale eurokonti, anbefaler mange **Wise** (tidligere TransferWise). Wise er en god løsning til:
- Internationale overførsler med lave gebyrer.
- At få en lokal konto i euro, som fungerer godt i hverdagen.
Hvis du planlægger at blive i Portugal i længere tid, kan det være praktisk at kombinere en lokal bankkonto med en online bank som Wise for at få det bedste fra begge verdener.

View File

@@ -0,0 +1,22 @@
---
name: Leveomkostninger på lavere indkomst i Portugal
description: Hvordan lave udgifter gør livet muligt på en lavere indkomst
author: Henrik Jess
date: ons 11 dec 23:30:00 CET 2024
summary: Lav bolig og billige varer gør livet overkommeligt
favorite: false
image: images/pic05.jpg
category: Leveomkostninger
tags: [Portugal, Leveomkostninger, Lav indkomst, Bolig, Mad, Transport]
---
# Kan man leve godt på en lavere indkomst i Portugal?
Ja, alt tyder på, at man kan leve godt i Portugal, selv på en lavere indkomst. Det handler især om lave boligomkostninger og de generelt overkommelige priser på mad og transport.
### Lavere leveomkostninger
- **Bolig**: Uden for de store byer som Lissabon og Porto kan huslejen være markant lavere.
- **Mad og dagligvarer**: Lokale produkter som frugt, grøntsager og fisk er både friske og billige.
- **Transport**: Offentlig transport er væsentligt billigere end i Danmark, og den er samtidig effektiv.
Hvis du kan acceptere et mere simpelt liv væk fra de mest populære områder, er Portugal et af de steder, hvor pengene rækker længere. Det gør landet til et attraktivt valg for dem, der ønsker at prioritere livskvalitet fremfor høje indkomster.

View File

@@ -0,0 +1,25 @@
---
name: Flytte permanent til Portugal
description: Praktiske trin for EU-borgere, der vil flytte til Portugal
author: Henrik Jess
date: ons 11 dec 23:40:00 CET 2024
summary: Registrering, NIF-nummer og praktiske krav for at flytte
favorite: false
image: images/pic04.jpg
category: Flytning
tags: [Portugal, Flytning, NIF-nummer, EU-borger, Bosiddende, Bankkonto, Arbejde]
---
# Hvad kræves for at flytte permanent til Portugal?
Som EU-borger er det relativt ukompliceret at flytte permanent til Portugal, men der er nogle formelle trin, du skal gennemføre:
1. **Registrering som bosiddende**: Inden for 90 dage efter ankomst skal du registrere dig som bosiddende i Portugal.
2. **NIF-nummer (skattenummer)**: Dette er nødvendigt for stort set alt at oprette en bankkonto, købe ejendom eller få arbejde.
3. **Dokumentation for økonomisk aktivitet**: En ansættelseskontrakt, bevis på selvstændig virksomhed eller pensionsindkomst kan være nødvendigt for at bekræfte din økonomiske stabilitet.
### Praktiske råd
- Sørg for at få styr på papirarbejdet hurtigt efter ankomst, da det kan spare dig for unødvendige problemer.
- Overvej at få hjælp fra lokale rådgivere, hvis du støder på udfordringer med sprog eller bureaukrati.
Portugal byder på en forholdsvis smidig proces for EU-borgere, hvilket gør det til et oplagt valg for dem, der ønsker at starte et nyt liv i et andet land.

View File

@@ -0,0 +1,44 @@
---
name: Lejligheder i Porto
description: Detaljer om lejligheder til leje i Porto med praktiske informationer for EU-borgere
author: Henrik Jess
date: ons 11 dec 23:40:00 CET 2024
summary: To lejligheder i Porto med specifikationer, priser og billeder
favorite: false
image: images/pic04.jpg
category: Bolig
tags: [Portugal, Bolig, Leje, Porto, EU-borger, Flytning]
---
# Lejlighed i Porto
Dette er en lejlighed beliggende i Porto på Rua 28 de Janeiro, Candal - Regadas, Santa Marinha e São Pedro da Afurada.
Lejligheden har en månedlig husleje på **1.200 euro** og indeholder følgende:
- **3 værelser**, hvoraf 1 er et ensuite-værelse
- **1 badeværelse**
- **1 gæstetoilet**
- **Stue og køkken** i åben plan
- **1 balkon**, der deles mellem alle fire værelser
- **1 balkon** tilknyttet køkkenet
- **1 opbevaringsrum** på 10 m²
- **2 parkeringspladser**
{{ slider(options={"width": 500, "height": 500}, images=["lejlighed2/Appartment_20250107_214352.png","lejlighed2/Appartment_20250107_214411.png","lejlighed2/Appartment_20250107_214423.png","lejlighed2/Appartment_20250107_214436.png","lejlighed2/Appartment_20250107_214446.png","lejlighed2/Appartment_20250107_214455.png","lejlighed2/Appartment_20250107_214504.png","lejlighed2/Appartment_20250107_214625.png","lejlighed2/Appartment_20250107_214639.png","lejlighed2/Appartment_20250107_214712.png","lejlighed2/Appartment_20250107_214733.png","lejlighed2/Appartment_20250107_214822.png","lejlighed2/Appartment_20250107_214843.png"]) }}
---
Dette er endnu lejlighed beliggende i Porto
Lejligheden har en månedlig husleje på **1.050 euro** og indeholder følgende:
- **3 værelser**
- **1 badeværelse**
- **1 gæstetoilet**
- **Stue og køkken** i åben plan
- **1 balkon**, der deles mellem alle fire værelser
- **1 balkon** tilknyttet køkkenet
{{ slider(options={"width": 500, "height": 500}, images=["lejlighed1/Appartment_20250106_205457-2.png","lejlighed1/Appartment_20250106_205634.png","lejlighed1/Appartment_20250106_205648.png","lejlighed1/Appartment_20250106_205657.png","lejlighed1/Appartment_20250106_205706.png","lejlighed1/Appartment_20250106_205714.png","lejlighed1/Appartment_20250106_205728-1.png","lejlighed1/Appartment_20250106_205744.png","lejlighed1/Appartment_20250106_205755.png","lejlighed1/Appartment_20250106_205806.png","lejlighed1/Appartment_20250106_205816.png","lejlighed1/Appartment_20250106_205835.png","lejlighed1/Appartment_20250106_205842.png","lejlighed1/Appartment_20250106_205852.png","lejlighed1/Appartment_20250106_205901.png","lejlighed1/Appartment_20250106_205909.png","lejlighed1/Appartment_20250106_205918.png","lejlighed1/Appartment_20250106_205928.png","lejlighed1/Appartment_20250106_205936.png","lejlighed1/Appartment_20250106_205946.png","lejlighed1/Appartment_20250106_205955.png"]) }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Some files were not shown because too many files have changed in this diff Show More