scriptorium 0.6.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/assets/icons/social/reddit.png +0 -0
- data/assets/icons/social/x-logo.png +0 -0
- data/assets/imagenotfound.jpg +0 -0
- data/bin/sblog +84 -5
- data/bin/scriptorium +1 -0
- data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +0 -1
- data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +0 -29
- data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +0 -19
- data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +1 -1
- data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +1 -1
- data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +1 -1
- data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +0 -10
- data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +1 -4
- data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
- data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
- data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
- data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
- data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
- data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
- data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
- data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
- data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
- data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
- data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
- data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
- data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
- data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
- data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
- data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
- data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
- data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
- data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
- data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
- data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
- data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
- data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
- data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
- data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
- data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
- data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
- data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
- data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
- data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
- data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
- data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
- data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
- data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
- data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
- data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
- data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
- data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
- data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
- data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
- data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
- data/doc/imported/0018-ruby-puns/post.html +46 -0
- data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
- data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
- data/doc/imported/0020-fffff/metadata.txt +7 -0
- data/doc/imported/0020-fffff/post.html +24 -0
- data/doc/imported/0020-fffff/source.lt3 +12 -0
- data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
- data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
- data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
- data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
- data/doc/imported/0023-doctor-sleep/post.html +63 -0
- data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
- data/doc/imported/0024-just-a-test/metadata.txt +7 -0
- data/doc/imported/0024-just-a-test/post.html +24 -0
- data/doc/imported/0024-just-a-test/source.lt3 +12 -0
- data/doc/imported/import_summary.txt +98 -0
- data/doc/livetext-informal-spec.txt +65 -0
- data/doc/myuserdoc/ch-0.lt3 +31 -0
- data/doc/myuserdoc/ch-1.lt3 +37 -0
- data/doc/myuserdoc/ch-10.lt3 +22 -0
- data/doc/myuserdoc/ch-2.lt3 +37 -0
- data/doc/myuserdoc/ch-3.lt3 +19 -0
- data/doc/myuserdoc/ch-4.lt3 +43 -0
- data/doc/myuserdoc/ch-5.lt3 +22 -0
- data/doc/myuserdoc/ch-6.lt3 +19 -0
- data/doc/myuserdoc/ch-7.lt3 +16 -0
- data/doc/myuserdoc/ch-8.lt3 +13 -0
- data/doc/myuserdoc/ch-9.lt3 +19 -0
- data/doc/myuserdoc/tweak.rb +18 -0
- data/doc/{userdoc-toc.txt → myuserdoc/userdoc-toc.txt} +27 -27
- data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
- data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
- data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
- data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
- data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
- data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
- data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
- data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
- data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
- data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
- data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
- data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
- data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
- data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
- data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
- data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
- data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
- data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
- data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
- data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
- data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
- data/doc/old-posts/0024-just-a-test.lt3 +12 -0
- data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
- data/doc/old-repo +1 -0
- data/doc/reddit_integration.md +2 -2
- data/doc/user.lt3 +0 -3
- data/lib/scriptorium/api.rb +1811 -78
- data/lib/scriptorium/banner_svg.rb +55 -68
- data/lib/scriptorium/contract.rb +3 -2
- data/lib/scriptorium/exceptions.rb +133 -102
- data/lib/scriptorium/helpers.rb +282 -82
- data/lib/scriptorium/post.rb +81 -17
- data/lib/scriptorium/reddit.rb +1 -1
- data/lib/scriptorium/repo.rb +478 -164
- data/lib/scriptorium/standard_files.rb +30 -396
- data/lib/scriptorium/support/common_js/clipboard.js +35 -0
- data/lib/scriptorium/support/common_js/content-loader.js +187 -0
- data/lib/scriptorium/support/common_js/navigation.js +52 -0
- data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
- data/lib/scriptorium/support/config/reddit_template.txt +17 -0
- data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/social.txt +1 -0
- data/lib/scriptorium/support/highlight/css.txt +2 -0
- data/lib/scriptorium/support/highlight/custom.css +119 -0
- data/lib/scriptorium/support/highlight/js.txt +1 -0
- data/lib/scriptorium/support/post_index/config.txt +15 -0
- data/lib/scriptorium/support/post_index/style.css +55 -0
- data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
- data/{test/scriptorium-TEST-1754622690-146/themes/standard/initial/post.lt3 → lib/scriptorium/support/templates/initial_post.lt3} +5 -5
- data/lib/scriptorium/support/templates/post.lt3 +104 -0
- data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/header.txt → lib/scriptorium/support/theme/header.lt3} +1 -1
- data/lib/scriptorium/theme.rb +83 -70
- data/lib/scriptorium/version.rb +2 -2
- data/lib/scriptorium/view.rb +194 -149
- data/lib/scriptorium.rb +24 -1
- data/lib/skeleton.rb +4 -1
- data/scriptorium.gemspec +2 -1
- data/test/WEB_INTEGRATION_README.md +196 -0
- data/test/all +40 -0
- data/test/banner_svg/unit.rb +267 -35
- data/test/config/deployment.txt +5 -0
- data/test/integration/integration_test.rb +7 -7
- data/test/integration/preview_flow_test.rb +94 -0
- data/test/livetext_plugin_test.rb +453 -182
- data/test/manual/banner-tests/test01.html +82 -18
- data/test/manual/banner-tests/test02.html +82 -18
- data/test/manual/banner-tests/test03.html +82 -18
- data/test/manual/banner-tests/test04.html +89 -25
- data/test/manual/banner-tests/test05.html +89 -25
- data/test/manual/banner-tests/test06.html +89 -25
- data/test/manual/banner-tests/test07.html +89 -25
- data/test/manual/banner-tests/test08.html +82 -18
- data/test/manual/banner-tests/test09.html +82 -18
- data/test/manual/banner-tests/test10.html +82 -18
- data/test/manual/banner-tests/test11.html +82 -18
- data/test/manual/banner-tests/test12.html +82 -18
- data/test/manual/banner-tests/test13.html +82 -18
- data/test/manual/banner-tests/test14.html +82 -18
- data/test/manual/banner-tests/test15.html +82 -18
- data/test/manual/banner-tests/test16.html +82 -18
- data/test/manual/banner-tests/test17.html +82 -18
- data/test/manual/banner-tests/test18.html +90 -26
- data/test/manual/banner-tests/test19.html +90 -26
- data/test/manual/banner-tests/test20.html +90 -26
- data/test/manual/banner-tests/test21.html +90 -26
- data/test/manual/banner-tests/test22.html +90 -26
- data/test/manual/banner-tests/test23.html +90 -26
- data/test/manual/banner-tests/test24.html +90 -26
- data/test/manual/banner-tests/test25.html +89 -25
- data/test/manual/banner_environment.rb +15 -2
- data/test/manual/codemirror_demo.html +773 -0
- data/test/manual/create_posts_for_web.rb +114 -0
- data/test/manual/preview_manual_test.rb +129 -0
- data/test/manual/test_banner_features.rb +14 -14
- data/test/manual/test_banner_integration.rb +115 -0
- data/test/manual/test_banner_radial.rb +87 -0
- data/test/manual/test_syntax_highlighting.rb +60 -40
- data/test/support/preview_utils.rb +88 -0
- data/test/test_gem_assets.rb +48 -0
- data/test/test_helpers.rb +10 -0
- data/test/tui_editor_integration_test.rb +15 -15
- data/test/tui_integration_test.rb +687 -441
- data/test/unit/api.rb +757 -37
- data/test/unit/asset_management.rb +195 -221
- data/test/unit/backup_test.rb +451 -0
- data/test/unit/contract_test.rb +1 -23
- data/test/unit/core.rb +415 -61
- data/test/unit/deploy_config_test.rb +248 -0
- data/test/unit/deploy_test.rb +312 -21
- data/test/unit/edit_post_test.rb +168 -0
- data/test/unit/gem_asset_management.rb +36 -42
- data/test/unit/livetext_basic.rb +23 -35
- data/test/unit/livetext_compatibility.rb +7 -14
- data/test/unit/parse_cmd_test.rb +260 -0
- data/test/unit/{symlink_test.rb → permalink_copy_test.rb} +47 -49
- data/test/unit/post.rb +91 -26
- data/test/unit/post_index_config_test.rb +258 -0
- data/test/unit/post_state_helpers_test.rb +137 -0
- data/test/unit/read_commented_file_test.rb +8 -6
- data/test/unit/repo.rb +75 -54
- data/test/unit/social_test.rb +41 -44
- data/test/unit/syntax_highlighting.rb +70 -0
- data/test/unit/theme_management_test.rb +91 -0
- data/test/unit/view.rb +79 -12
- data/test/unit/widgets.rb +8 -8
- data/test/web_integration_test.rb +231 -0
- data/test/web_test_helper.rb +218 -0
- data/test/web_workflow_test.rb +527 -0
- data/ui/tui/bin/scriptorium +885 -415
- data/ui/web/app/app.rb +1398 -176
- data/ui/web/app/assets/livetext_mode.js +244 -0
- data/ui/web/app/error_helpers.rb +16 -16
- data/ui/web/app/views/advanced_config.erb +8 -2
- data/ui/web/app/views/asset_management.erb +56 -0
- data/ui/web/app/views/backup_management.erb +238 -0
- data/ui/web/app/views/config_widget.erb +232 -0
- data/ui/web/app/views/dashboard.erb +64 -72
- data/ui/web/app/views/deploy_config.erb +3 -0
- data/ui/web/app/views/edit_pages.erb +170 -2
- data/ui/web/app/views/edit_post.erb +130 -9
- data/ui/web/app/views/edit_theme.erb +73 -0
- data/ui/web/app/views/edit_theme_file.erb +74 -0
- data/ui/web/app/views/theme_management.erb +130 -0
- data/ui/web/app/views/view_dashboard.erb +666 -25
- data/ui/web/app/views/widgets.erb +249 -0
- data/ui/web/bin/scriptorium-web +35 -24
- data/ui/web/tmp/timing.log +17 -0
- data/ui/web/tmp/web_server.log +0 -5
- metadata +190 -116
- data/assets/back-icon.png +0 -0
- data/assets/icons/facebook.svg +0 -1
- data/assets/icons/github.svg +0 -1
- data/assets/icons/instagram.svg +0 -1
- data/assets/icons/reddit.svg +0 -1
- data/assets/icons/x.svg +0 -1
- data/assets/icons/youtube.svg +0 -1
- data/bin/scriptorium +0 -1511
- data/doc/anti-amnesia/20250727-060000-api-design-tui-planning.md +0 -34
- data/doc/anti-amnesia/20250727-061000-runeblog-tui-analysis.md +0 -50
- data/doc/anti-amnesia/20250727-154000-livetext-plugin-file-stats.md +0 -73
- data/doc/anti-amnesia/20250727-172600-unified-minitest-framework.md +0 -70
- data/doc/anti-amnesia/20250727-173000-widget-testing-achievement.md +0 -110
- data/doc/anti-amnesia/20250727-180000-post-id-num-refactoring.md +0 -73
- data/doc/anti-amnesia/20250728-124421-conversation-summary-concise.md +0 -124
- data/doc/anti-amnesia/20250729-190000-scriptorium-tui-testing-complete.md +0 -46
- data/doc/anti-amnesia/20250729-200000-scriptorium-tui-testing-edit-file-workflow.md +0 -97
- data/doc/anti-amnesia/20250729-211500-dependency-management-system.md +0 -211
- data/doc/anti-amnesia/20250729-213000-python-virtual-environment-setup.md +0 -141
- data/doc/anti-amnesia/20250729-214500-theme-management-commands.md +0 -211
- data/doc/anti-amnesia/20250729-215000-version-update-to-0.6.0.md +0 -134
- data/doc/anti-amnesia/20250729-220000-user-guide-complete.md +0 -41
- data/doc/anti-amnesia/20250804-213700-publishing-test-fix.md +0 -49
- data/doc/anti-amnesia/20250804-214400-additional-test-fixes.md +0 -46
- data/doc/anti-amnesia/20250804-220000-asset-function-logic-clarification.md +0 -41
- data/doc/anti-amnesia/20250806-202032-asset-function-logic-clarification.md +0 -41
- data/doc/anti-amnesia/20250813-082428-syntax-highlighting-and-navigation-improvements.md +0 -256
- data/lib/scriptorium/syntax_highlighter.rb +0 -234
- data/test/manual/deploy_symlink_demo.rb +0 -142
- data/test/manual/symlink_demo.rb +0 -117
- data/test/manual/test2.rb +0 -12
- data/test/manual/test_banner_from_file.rb +0 -150
- data/test/manual/test_banner_in_header.rb +0 -35
- data/test/manual/test_code_highlighting.rb +0 -68
- data/test/manual/test_complex_header.rb +0 -74
- data/test/manual/test_empty_header.rb +0 -32
- data/test/manual/test_radial_custom.rb +0 -58
- data/test/manual/test_radial_large_radius.rb +0 -52
- data/test/manual/test_svg_debug.rb +0 -47
- data/test/pages-demo/config/currentview.txt +0 -1
- data/test/pages-demo/views/demo/config/common.js +0 -57
- data/test/pages-demo/views/demo/config/footer.txt +0 -1
- data/test/pages-demo/views/demo/config/global-head.txt +0 -8
- data/test/pages-demo/views/demo/config/header.txt +0 -1
- data/test/pages-demo/views/demo/config/layout.txt +0 -1
- data/test/pages-demo/views/demo/config/left.txt +0 -1
- data/test/pages-demo/views/demo/config/main.txt +0 -1
- data/test/pages-demo/views/demo/config/right.txt +0 -1
- data/test/pages-demo/views/demo/config.txt +0 -3
- data/test/pages-demo/views/demo/output/panes/footer.html +0 -1
- data/test/pages-demo/views/demo/output/panes/header.html +0 -1
- data/test/pages-demo/views/demo/output/panes/left.html +0 -1
- data/test/pages-demo/views/demo/output/panes/main.html +0 -1
- data/test/pages-demo/views/demo/output/panes/right.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_css.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/config/bootstrap_js.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/config/common.js +0 -57
- data/test/scriptorium-TEST-1754622690-146/config/currentview.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/global-head.txt +0 -9
- data/test/scriptorium-TEST-1754622690-146/config/last_post_num.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/config/os_helpers.rb +0 -4
- data/test/scriptorium-TEST-1754622690-146/config/widgets.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/posts/0001/meta.txt +0 -8
- data/test/scriptorium-TEST-1754622690-146/posts/0001/source.lt3 +0 -6
- data/test/scriptorium-TEST-1754622690-146/themes/standard/README.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/config.txt +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/layout/gen/text.css +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index.lt3 +0 -1
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/index_entry.lt3 +0 -14
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/post.lt3 +0 -13
- data/test/scriptorium-TEST-1754622690-146/themes/standard/templates/widget.lt3 +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_css.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/bootstrap_js.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/common.js +0 -57
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/deploy.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/footer.txt +0 -2
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/global-head.txt +0 -9
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/header.txt +0 -4
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/layout.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/left.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/main.txt +0 -5
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/right.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/config/status.txt +0 -7
- data/test/scriptorium-TEST-1754622690-146/views/sample/config.txt +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/footer.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/header.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/left.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/main.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/layout/right.html +0 -3
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/footer.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/header.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/left.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/main.html +0 -1
- data/test/scriptorium-TEST-1754622690-146/views/sample/output/panes/right.html +0 -1
- data/ui/web/tmp/web_server.pid +0 -1
- /data/{test/pages-demo/views/demo/config/bootstrap_css.txt → lib/scriptorium/support/bootstrap/css.txt} +0 -0
- /data/{test/pages-demo/views/demo/config/bootstrap_js.txt → lib/scriptorium/support/bootstrap/js.txt} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/views/sample → lib/scriptorium/support}/config/reddit.txt +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout → lib/scriptorium/support/templates}/layout.txt +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/footer.txt → lib/scriptorium/support/theme/footer.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/left.txt → lib/scriptorium/support/theme/left.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/main.txt → lib/scriptorium/support/theme/main.lt3} +0 -0
- /data/{test/scriptorium-TEST-1754622690-146/themes/standard/layout/config/right.txt → lib/scriptorium/support/theme/right.lt3} +0 -0
- /data/test/manual/banner-tests/{config.txt → svg.txt} +0 -0
- /data/test/manual/{test6.rb → test_advanced_widgets.rb} +0 -0
- /data/test/manual/{test1.rb → test_basic_posts.rb} +0 -0
- /data/test/manual/{test4.rb → test_layout_widgets.rb} +0 -0
- /data/test/manual/{test5.rb → test_pagination.rb} +0 -0
- /data/test/manual/{test3.rb → test_random_posts.rb} +0 -0
data/ui/web/app/app.rb
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
# Parse command line arguments for test mode BEFORE requiring Sinatra
|
6
|
+
# Starting web app, ARGV: #{ARGV.inspect}
|
7
|
+
TEST_MODE = false
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.on("--test", "Use test repository (scriptorium-TEST)") do
|
10
|
+
TEST_MODE = true
|
11
|
+
# --test flag detected
|
12
|
+
end
|
13
|
+
end.parse!
|
14
|
+
|
15
|
+
# Command line parsing complete, test_mode: #{TEST_MODE}
|
16
|
+
# ARGV remaining: #{ARGV.inspect}
|
17
|
+
|
3
18
|
require 'sinatra'
|
4
19
|
require 'sinatra/reloader' if development?
|
5
20
|
require 'fileutils'
|
@@ -12,14 +27,59 @@ end
|
|
12
27
|
require_relative '../../../lib/scriptorium'
|
13
28
|
require_relative 'error_helpers'
|
14
29
|
|
15
|
-
include ErrorHelpers
|
16
|
-
|
17
30
|
class ScriptoriumWeb < Sinatra::Base
|
31
|
+
include ErrorHelpers
|
32
|
+
include Scriptorium::Helpers
|
33
|
+
|
18
34
|
set :port, 4567
|
19
35
|
set :bind, '0.0.0.0'
|
20
36
|
set :views, File.join(__dir__, 'views')
|
21
37
|
set :show_exceptions, false # Disable Sinatra's default error display
|
22
38
|
|
39
|
+
# Configure static file serving for assets
|
40
|
+
configure do
|
41
|
+
# Set public folder to serve static files from the current view's output directory
|
42
|
+
# This will be updated dynamically based on the current view
|
43
|
+
set :public_folder, File.join(__dir__, '..', 'scriptorium-TEST', 'views', 'computing', 'output')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Update static file serving based on current view
|
47
|
+
before do
|
48
|
+
if @api&.current_view
|
49
|
+
public_path = @api.root/"views"/@api.current_view.name/"output"
|
50
|
+
if Dir.exist?(public_path)
|
51
|
+
settings.public_folder = public_path.to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Helper method to render dashboard with error/message
|
57
|
+
def render_dashboard(error: nil, message: nil)
|
58
|
+
@error = error
|
59
|
+
@message = message
|
60
|
+
@current_view = @api&.current_view
|
61
|
+
@views = @api&.views || []
|
62
|
+
@posts = []
|
63
|
+
erb :dashboard
|
64
|
+
end
|
65
|
+
|
66
|
+
# Helper method to add file/line info to error messages
|
67
|
+
def error_with_location(error, message)
|
68
|
+
error_location = "#{error.backtrace.first}" if error.backtrace
|
69
|
+
result = message
|
70
|
+
result += " (#{error_location})" if error_location
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
# Set test mode
|
75
|
+
def self.test_mode=(value)
|
76
|
+
@@test_mode = value
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.test_mode
|
80
|
+
@@test_mode
|
81
|
+
end
|
82
|
+
|
23
83
|
# Enable reloading in development
|
24
84
|
configure :development do
|
25
85
|
register Sinatra::Reloader
|
@@ -36,11 +96,24 @@ class ScriptoriumWeb < Sinatra::Base
|
|
36
96
|
# Initialize API instance
|
37
97
|
before do
|
38
98
|
begin
|
39
|
-
|
40
|
-
#
|
41
|
-
|
42
|
-
|
99
|
+
# Use the TEST_MODE constant that was set before OptionParser consumed ARGV
|
100
|
+
# Before block - test_mode: #{TEST_MODE}
|
101
|
+
@api = Scriptorium::API.new(testmode: TEST_MODE)
|
102
|
+
|
103
|
+
if TEST_MODE
|
104
|
+
# Use test repository in the ui/web/ directory
|
105
|
+
test_repo_path = File.join(__dir__, "..", "scriptorium-TEST")
|
106
|
+
# Opening test repo: #{test_repo_path}
|
107
|
+
@api.open_repo(test_repo_path) if Dir.exist?(test_repo_path)
|
108
|
+
else
|
109
|
+
# Use production repository
|
110
|
+
home = ENV['HOME']
|
111
|
+
production_path = "#{home}/.scriptorium"
|
112
|
+
# Opening production repo: #{production_path}
|
113
|
+
@api.open_repo(production_path) if Dir.exist?(production_path)
|
114
|
+
end
|
43
115
|
rescue => e
|
116
|
+
# Error in before block: #{e.message}
|
44
117
|
@api = nil
|
45
118
|
end
|
46
119
|
end
|
@@ -54,10 +127,13 @@ class ScriptoriumWeb < Sinatra::Base
|
|
54
127
|
|
55
128
|
# Only try to load posts if we have a current view
|
56
129
|
if @current_view
|
57
|
-
|
130
|
+
File.write("/tmp/debug.log", "DEBUG: Route reached, current_view: #{@current_view.name}\n", mode: 'a')
|
131
|
+
@posts = @api.posts(@current_view.name, include_deleted: true) || []
|
132
|
+
File.write("/tmp/debug.log", "DEBUG: Posts loaded: #{@posts.length}\n", mode: 'a')
|
58
133
|
if @posts.length > 0
|
59
134
|
end
|
60
135
|
else
|
136
|
+
File.write("/tmp/debug.log", "DEBUG: No current view\n", mode: 'a')
|
61
137
|
@posts = []
|
62
138
|
end
|
63
139
|
else
|
@@ -77,16 +153,16 @@ class ScriptoriumWeb < Sinatra::Base
|
|
77
153
|
view_name = params[:view_name]
|
78
154
|
|
79
155
|
if view_name.nil? || view_name.strip.empty?
|
80
|
-
|
156
|
+
render_dashboard(error: "No view selected")
|
81
157
|
return
|
82
158
|
end
|
83
159
|
|
84
160
|
begin
|
85
161
|
view = @api.lookup_view(view_name)
|
86
162
|
@api.view(view_name)
|
87
|
-
|
163
|
+
render_dashboard(message: "View changed successfully")
|
88
164
|
rescue => e
|
89
|
-
|
165
|
+
render_dashboard(error: error_with_location(e, "Failed to change view: #{e.message}"))
|
90
166
|
end
|
91
167
|
end
|
92
168
|
|
@@ -130,29 +206,32 @@ class ScriptoriumWeb < Sinatra::Base
|
|
130
206
|
return
|
131
207
|
end
|
132
208
|
|
209
|
+
# Get selected views from checkboxes
|
210
|
+
selected_views = params[:views] || [current_view.name]
|
211
|
+
selected_views = [current_view.name] if selected_views.empty?
|
212
|
+
|
213
|
+
# Process tags
|
214
|
+
tags = params[:tags]&.strip
|
215
|
+
tags = tags&.split(',')&.map(&:strip) if tags && !tags.empty?
|
216
|
+
|
133
217
|
# Create a draft first
|
134
218
|
draft_path = @api.create_draft(
|
135
219
|
title: params[:title].strip,
|
136
220
|
body: "", # Empty body to start
|
137
|
-
views:
|
138
|
-
tags:
|
139
|
-
blurb:
|
221
|
+
views: selected_views,
|
222
|
+
tags: tags,
|
223
|
+
blurb: params[:blurb]&.strip
|
140
224
|
)
|
141
225
|
|
142
226
|
# Convert draft to post immediately
|
143
227
|
post_num = @api.finish_draft(draft_path)
|
144
228
|
# Generate the post to create meta.txt and other files
|
145
229
|
begin
|
146
|
-
STDERR.puts "DEBUG: About to call generate_post for post #{post_num}"
|
147
|
-
STDERR.puts "DEBUG: Current working directory: #{Dir.pwd}"
|
148
|
-
STDERR.puts "DEBUG: API root: #{@api.root}"
|
149
230
|
@api.generate_post(post_num)
|
150
|
-
STDERR.puts "DEBUG: generate_post completed successfully"
|
151
231
|
# Check if meta.txt was created
|
152
232
|
meta_file = @api.root/"posts"/"#{post_num.to_s.rjust(4, '0')}"/"meta.txt"
|
153
|
-
|
154
|
-
|
155
|
-
redirect "/?message=Post '#{params[:title].strip}' created successfully (##{post_num})"
|
233
|
+
# Redirect back to dashboard with modal parameter to open CodeMirror editor
|
234
|
+
redirect "/view/#{current_view.name}?edit_post=#{post_num}"
|
156
235
|
rescue => e
|
157
236
|
# Log the actual error for debugging
|
158
237
|
STDERR.puts "ERROR in generate_post: #{e.class}: #{e.message}"
|
@@ -182,14 +261,46 @@ class ScriptoriumWeb < Sinatra::Base
|
|
182
261
|
return
|
183
262
|
end
|
184
263
|
|
185
|
-
# Redirect to the
|
186
|
-
|
264
|
+
# Redirect back to the view dashboard
|
265
|
+
current_view = @api&.current_view
|
266
|
+
if current_view
|
267
|
+
redirect "/view/#{current_view.name}?message=Post saved successfully"
|
268
|
+
else
|
269
|
+
redirect "/?message=Post saved successfully"
|
270
|
+
end
|
187
271
|
rescue => e
|
188
272
|
error_info = friendly_error_message(e)
|
189
273
|
redirect "/?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
190
274
|
end
|
191
275
|
end
|
192
276
|
|
277
|
+
# API endpoint to get post content for modal
|
278
|
+
get '/api/post_content/:id' do
|
279
|
+
begin
|
280
|
+
post_id = params[:id].to_i
|
281
|
+
post = @api.post(post_id)
|
282
|
+
|
283
|
+
if post.nil?
|
284
|
+
status 404
|
285
|
+
return "Post not found"
|
286
|
+
end
|
287
|
+
|
288
|
+
# Read the source file
|
289
|
+
source_file = @api.root/"posts"/"#{post.num.to_s.rjust(4, '0')}"/"source.lt3"
|
290
|
+
if File.exist?(source_file)
|
291
|
+
content = File.read(source_file)
|
292
|
+
content_type :text
|
293
|
+
content
|
294
|
+
else
|
295
|
+
status 404
|
296
|
+
"Source file not found"
|
297
|
+
end
|
298
|
+
rescue => e
|
299
|
+
status 500
|
300
|
+
"Error loading post content: #{e.message}"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
193
304
|
# Show edit post page
|
194
305
|
get '/edit_post/:id' do
|
195
306
|
post_id = params[:id]&.to_i
|
@@ -209,11 +320,14 @@ class ScriptoriumWeb < Sinatra::Base
|
|
209
320
|
# Read the source file content
|
210
321
|
source_file = @api.root/"posts"/@post.num/"source.lt3"
|
211
322
|
if File.exist?(source_file)
|
212
|
-
@content =
|
323
|
+
@content = read_file(source_file)
|
213
324
|
else
|
214
325
|
@content = "# #{@post.title}\n\n"
|
215
326
|
end
|
216
327
|
|
328
|
+
# Set current view for template
|
329
|
+
@current_view = @api&.current_view
|
330
|
+
|
217
331
|
erb :edit_post
|
218
332
|
rescue => e
|
219
333
|
redirect "/?error=Failed to load post: #{e.message}"
|
@@ -222,38 +336,84 @@ class ScriptoriumWeb < Sinatra::Base
|
|
222
336
|
|
223
337
|
# Save edited post
|
224
338
|
post '/save_post/:id' do
|
225
|
-
post_id = params[:id]&.to_i
|
226
|
-
content = params[:content]
|
227
|
-
|
228
|
-
if post_id.nil? || post_id <= 0
|
229
|
-
redirect "/?error=Invalid post ID"
|
230
|
-
return
|
231
|
-
end
|
232
|
-
|
233
|
-
if content.nil?
|
234
|
-
redirect "/edit_post/#{post_id}?error=No content provided"
|
235
|
-
return
|
236
|
-
end
|
237
|
-
|
238
339
|
begin
|
340
|
+
File.write('/tmp/save_post_debug.log', "=== SAVE POST ATTEMPT ===\n", mode: 'a')
|
341
|
+
File.write('/tmp/save_post_debug.log', "Time: #{Time.now}\n", mode: 'a')
|
342
|
+
File.write('/tmp/save_post_debug.log', "Post ID: #{params[:id]}\n", mode: 'a')
|
343
|
+
File.write('/tmp/save_post_debug.log', "Content length: #{params[:content]&.length || 0}\n", mode: 'a')
|
344
|
+
|
345
|
+
File.write('/tmp/save_post_debug.log', "API instance: #{@api.inspect}\n", mode: 'a')
|
346
|
+
|
347
|
+
post_id = params[:id]&.to_i
|
348
|
+
content = params[:content]
|
349
|
+
|
350
|
+
if post_id.nil? || post_id <= 0
|
351
|
+
File.write('/tmp/save_post_debug.log', "ERROR: Invalid post ID\n", mode: 'a')
|
352
|
+
redirect "/?error=Invalid post ID"
|
353
|
+
return
|
354
|
+
end
|
355
|
+
|
356
|
+
if content.nil?
|
357
|
+
File.write('/tmp/save_post_debug.log', "ERROR: No content provided\n", mode: 'a')
|
358
|
+
redirect "/edit_post/#{post_id}?error=No content provided"
|
359
|
+
return
|
360
|
+
end
|
361
|
+
|
362
|
+
File.write('/tmp/save_post_debug.log', "Looking up post #{post_id}\n", mode: 'a')
|
239
363
|
post = @api.post(post_id)
|
240
364
|
if post.nil?
|
365
|
+
File.write('/tmp/save_post_debug.log', "ERROR: Post not found\n", mode: 'a')
|
241
366
|
redirect "/?error=Post not found"
|
242
367
|
return
|
243
368
|
end
|
244
369
|
|
370
|
+
File.write('/tmp/save_post_debug.log', "Post found: #{post.inspect}\n", mode: 'a')
|
371
|
+
File.write('/tmp/save_post_debug.log', "Post num: #{post.num}\n", mode: 'a')
|
372
|
+
|
245
373
|
# Write the content to the source file
|
246
374
|
source_file = @api.root/"posts"/post.num/"source.lt3"
|
247
|
-
File.write(source_file,
|
375
|
+
File.write('/tmp/save_post_debug.log', "Source file: #{source_file}\n", mode: 'a')
|
376
|
+
write_file(source_file, content)
|
377
|
+
File.write('/tmp/save_post_debug.log', "File written successfully\n", mode: 'a')
|
248
378
|
|
249
379
|
# Generate the post after saving
|
250
|
-
|
380
|
+
File.write('/tmp/save_post_debug.log', "Generating post...\n", mode: 'a')
|
381
|
+
begin
|
382
|
+
@api.generate_post(post_id)
|
383
|
+
File.write('/tmp/save_post_debug.log', "Post generated successfully\n", mode: 'a')
|
384
|
+
rescue => e
|
385
|
+
File.write('/tmp/save_post_debug.log', "Generate post failed: #{e.class}: #{e.message}\n", mode: 'a')
|
386
|
+
File.write('/tmp/save_post_debug.log', "Backtrace: #{e.backtrace.first(3).join("\n")}\n", mode: 'a')
|
387
|
+
raise e
|
388
|
+
end
|
251
389
|
|
252
|
-
|
390
|
+
# Regenerate the view index to include the updated post
|
391
|
+
File.write('/tmp/save_post_debug.log', "Regenerating view index...\n", mode: 'a')
|
392
|
+
begin
|
393
|
+
current_view = @api&.current_view
|
394
|
+
if current_view
|
395
|
+
@api.generate_view(current_view.name)
|
396
|
+
File.write('/tmp/save_post_debug.log', "View index regenerated successfully\n", mode: 'a')
|
397
|
+
end
|
398
|
+
rescue => e
|
399
|
+
File.write('/tmp/save_post_debug.log', "View regeneration failed: #{e.class}: #{e.message}\n", mode: 'a')
|
400
|
+
# Don't fail the save if view regeneration fails
|
401
|
+
end
|
402
|
+
|
403
|
+
File.write('/tmp/save_post_debug.log', "SUCCESS: Redirecting to view dashboard\n", mode: 'a')
|
404
|
+
current_view = @api&.current_view
|
405
|
+
if current_view
|
406
|
+
redirect "/view/#{current_view.name}?message=Post saved successfully"
|
407
|
+
else
|
408
|
+
redirect "/?message=Post ##{post_id} saved and generated successfully"
|
409
|
+
end
|
253
410
|
rescue => e
|
254
|
-
|
255
|
-
|
411
|
+
File.write('/tmp/save_post_debug.log', "EXCEPTION: #{e.class}: #{e.message}\n", mode: 'a')
|
412
|
+
File.write('/tmp/save_post_debug.log', "Backtrace: #{e.backtrace.first(5).join("\n")}\n", mode: 'a')
|
413
|
+
error_location = e.backtrace&.first || "unknown location"
|
414
|
+
redirect "/edit_post/#{post_id}?error=Failed to save post: #{e.message} at #{error_location}"
|
256
415
|
end
|
416
|
+
end
|
257
417
|
|
258
418
|
# Generate post
|
259
419
|
post '/generate_post' do
|
@@ -285,44 +445,86 @@ class ScriptoriumWeb < Sinatra::Base
|
|
285
445
|
|
286
446
|
begin
|
287
447
|
if view_name.nil? || view_name.strip.empty?
|
288
|
-
|
448
|
+
render_dashboard(error: "No view specified")
|
289
449
|
return
|
290
450
|
end
|
291
451
|
|
292
452
|
# Generate the view
|
293
453
|
@api.generate_view(view_name)
|
294
|
-
|
454
|
+
render_dashboard(message: "View '#{view_name}' generated successfully")
|
295
455
|
rescue => e
|
296
|
-
|
456
|
+
render_dashboard(error: error_with_location(e, "Failed to generate view: #{e.message}"))
|
297
457
|
end
|
298
458
|
end
|
299
459
|
|
300
460
|
# Preview view
|
301
|
-
|
461
|
+
get '/preview' do
|
462
|
+
@current_view = @api&.current_view
|
463
|
+
if @current_view.nil?
|
464
|
+
render_dashboard(error: "No view selected. Please select a view first.")
|
465
|
+
return
|
466
|
+
end
|
467
|
+
|
468
|
+
begin
|
469
|
+
# Generate the view first to ensure it's up to date
|
470
|
+
@api.generate_view(@current_view.name)
|
471
|
+
|
472
|
+
# Redirect to the index route under /preview/:view_name so relative links resolve
|
473
|
+
redirect "/preview/#{@current_view.name}/index.html"
|
474
|
+
rescue => e
|
475
|
+
render_dashboard(error: error_with_location(e, "Failed to preview view: #{e.message}"))
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
# Preview specific view index
|
480
|
+
get '/preview/:view_name/index.html' do
|
302
481
|
view_name = params[:view_name]
|
303
482
|
|
304
483
|
begin
|
305
484
|
if view_name.nil? || view_name.strip.empty?
|
306
|
-
|
307
|
-
return
|
485
|
+
status 400
|
486
|
+
return "Bad request: missing view name"
|
308
487
|
end
|
309
488
|
|
310
|
-
# Generate the view
|
489
|
+
# Generate the view to ensure it's up to date
|
311
490
|
@api.generate_view(view_name)
|
312
491
|
|
313
|
-
#
|
314
|
-
|
315
|
-
index_file = view_dir/"output"/"index.html"
|
316
|
-
|
492
|
+
# Serve the generated index.html file
|
493
|
+
index_file = @api.root/"views"/view_name/"output"/"index.html"
|
317
494
|
if File.exist?(index_file)
|
318
|
-
# Return the HTML content directly for preview
|
319
495
|
content_type :html
|
320
|
-
|
496
|
+
read_file(index_file)
|
497
|
+
else
|
498
|
+
status 404
|
499
|
+
"Index file not found for view: #{view_name}"
|
500
|
+
end
|
501
|
+
rescue => e
|
502
|
+
status 500
|
503
|
+
"Error loading view: #{e.message}"
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
# Serve post_index.html fragment for SPA back navigation
|
508
|
+
get '/preview/:view_name/post_index.html' do
|
509
|
+
view_name = params[:view_name]
|
510
|
+
begin
|
511
|
+
if view_name.nil? || view_name.strip.empty?
|
512
|
+
status 400
|
513
|
+
return "Bad request: missing view name"
|
514
|
+
end
|
515
|
+
# Ensure view is generated
|
516
|
+
@api.generate_view(view_name)
|
517
|
+
fragment = @api.root/"views"/view_name/"output"/"post_index.html"
|
518
|
+
if File.exist?(fragment)
|
519
|
+
content_type :html
|
520
|
+
read_file(fragment)
|
321
521
|
else
|
322
|
-
|
522
|
+
status 404
|
523
|
+
"Not found"
|
323
524
|
end
|
324
525
|
rescue => e
|
325
|
-
|
526
|
+
status 500
|
527
|
+
"Error loading post_index: #{e.message}"
|
326
528
|
end
|
327
529
|
end
|
328
530
|
|
@@ -331,35 +533,267 @@ class ScriptoriumWeb < Sinatra::Base
|
|
331
533
|
view_name = params[:view_name]
|
332
534
|
filename = params[:filename]
|
333
535
|
|
334
|
-
|
536
|
+
begin
|
537
|
+
if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
|
538
|
+
status 404
|
539
|
+
return "File not found"
|
540
|
+
end
|
541
|
+
|
542
|
+
# Check if view has been generated (index.html exists)
|
543
|
+
index_file = @api.root/"views"/view_name/"output"/"index.html"
|
544
|
+
unless File.exist?(index_file)
|
545
|
+
status 404
|
546
|
+
return "View '#{view_name}' has not been generated. Please generate the view first."
|
547
|
+
end
|
548
|
+
|
549
|
+
# Construct the file path
|
550
|
+
post_file = @api.root/"views"/view_name/"output"/"posts"/filename
|
551
|
+
|
552
|
+
if File.exist?(post_file)
|
553
|
+
content_type :html
|
554
|
+
read_file(post_file)
|
555
|
+
else
|
556
|
+
status 404
|
557
|
+
"File not found: #{filename}"
|
558
|
+
end
|
559
|
+
rescue => e
|
560
|
+
status 500
|
561
|
+
"Error loading file: #{e.message}"
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
# Serve post content for iframe (with syntax highlighting)
|
566
|
+
get '/preview/:view_name/posts/:filename/content' do
|
567
|
+
view_name = params[:view_name]
|
568
|
+
filename = params[:filename]
|
335
569
|
|
336
570
|
begin
|
337
571
|
if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
|
338
|
-
STDERR.puts "DEBUG: Missing parameters"
|
339
572
|
status 404
|
340
573
|
return "File not found"
|
341
574
|
end
|
342
575
|
|
343
576
|
# Construct the file path
|
344
577
|
post_file = @api.root/"views"/view_name/"output"/"posts"/filename
|
345
|
-
STDERR.puts "DEBUG: Looking for file: #{post_file}"
|
346
|
-
STDERR.puts "DEBUG: File exists: #{File.exist?(post_file)}"
|
347
578
|
|
348
579
|
if File.exist?(post_file)
|
349
580
|
content_type :html
|
350
|
-
|
581
|
+
|
582
|
+
# Read the post content
|
583
|
+
post_content = read_file(post_file)
|
584
|
+
|
585
|
+
# Wrap in HTML document with syntax highlighting (Highlight.js)
|
586
|
+
html = <<~HTML
|
587
|
+
<!DOCTYPE html>
|
588
|
+
<html>
|
589
|
+
<head>
|
590
|
+
<meta charset="utf-8">
|
591
|
+
<title>Post Content</title>
|
592
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
593
|
+
<style>
|
594
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; line-height: 1.6; }
|
595
|
+
pre { background: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
596
|
+
code { background: #f5f5f5; padding: 2px 4px; border-radius: 3px; }
|
597
|
+
</style>
|
598
|
+
</head>
|
599
|
+
<body>
|
600
|
+
#{post_content}
|
601
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
602
|
+
<script>
|
603
|
+
document.addEventListener('DOMContentLoaded', function() {
|
604
|
+
if (typeof hljs !== 'undefined') { hljs.highlightAll(); }
|
605
|
+
});
|
606
|
+
</script>
|
607
|
+
</body>
|
608
|
+
</html>
|
609
|
+
HTML
|
610
|
+
|
611
|
+
html
|
612
|
+
else
|
613
|
+
status 404
|
614
|
+
"File not found: #{filename}"
|
615
|
+
end
|
616
|
+
rescue => e
|
617
|
+
status 500
|
618
|
+
"Error loading file: #{e.message}"
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
# Serve permalink files for preview
|
623
|
+
get '/preview/:view_name/permalink/:filename' do
|
624
|
+
view_name = params[:view_name]
|
625
|
+
filename = params[:filename]
|
626
|
+
|
627
|
+
begin
|
628
|
+
if view_name.nil? || view_name.strip.empty? || filename.nil? || filename.strip.empty?
|
629
|
+
status 404
|
630
|
+
return "File not found"
|
631
|
+
end
|
632
|
+
|
633
|
+
# Check if view has been generated (index.html exists)
|
634
|
+
index_file = @api.root/"views"/view_name/"output"/"index.html"
|
635
|
+
unless File.exist?(index_file)
|
636
|
+
status 404
|
637
|
+
return "View '#{view_name}' has not been generated. Please generate the view first."
|
638
|
+
end
|
639
|
+
|
640
|
+
# Construct the file path
|
641
|
+
permalink_file = @api.root/"views"/view_name/"output"/"permalink"/filename
|
642
|
+
|
643
|
+
if File.exist?(permalink_file)
|
644
|
+
content_type :html
|
645
|
+
read_file(permalink_file)
|
646
|
+
else
|
647
|
+
status 404
|
648
|
+
"File not found: #{filename}"
|
649
|
+
end
|
650
|
+
rescue => e
|
651
|
+
status 500
|
652
|
+
"Error loading file: #{e.message}"
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
# Serve assets for preview
|
657
|
+
get '/preview/:view_name/assets/*' do
|
658
|
+
view_name = params[:view_name]
|
659
|
+
asset_path = params[:splat].first
|
660
|
+
|
661
|
+
begin
|
662
|
+
if view_name.nil? || view_name.strip.empty? || asset_path.nil? || asset_path.strip.empty?
|
663
|
+
status 404
|
664
|
+
return "Asset not found"
|
665
|
+
end
|
666
|
+
|
667
|
+
# Check if view has been generated (index.html exists)
|
668
|
+
index_file = @api.root/"views"/view_name/"output"/"index.html"
|
669
|
+
unless File.exist?(index_file)
|
670
|
+
status 404
|
671
|
+
return "View '#{view_name}' has not been generated. Please generate the view first."
|
672
|
+
end
|
673
|
+
|
674
|
+
# Construct the asset file path (serve from generated output assets)
|
675
|
+
asset_file = @api.root/"views"/view_name/"output"/"assets"/asset_path
|
676
|
+
# Fallback: if not present in output assets, try view assets directly
|
677
|
+
unless File.exist?(asset_file)
|
678
|
+
fallback_asset = @api.root/"views"/view_name/"assets"/asset_path
|
679
|
+
asset_file = fallback_asset if File.exist?(fallback_asset)
|
680
|
+
end
|
681
|
+
|
682
|
+
if File.exist?(asset_file)
|
683
|
+
# Set appropriate content type based on file extension
|
684
|
+
case File.extname(asset_file).downcase
|
685
|
+
when '.png'
|
686
|
+
content_type 'image/png'
|
687
|
+
when '.jpg', '.jpeg'
|
688
|
+
content_type 'image/jpeg'
|
689
|
+
when '.gif'
|
690
|
+
content_type 'image/gif'
|
691
|
+
when '.svg'
|
692
|
+
content_type 'image/svg+xml'
|
693
|
+
when '.css'
|
694
|
+
content_type 'text/css'
|
695
|
+
when '.js'
|
696
|
+
content_type 'application/javascript'
|
697
|
+
else
|
698
|
+
content_type 'application/octet-stream'
|
699
|
+
end
|
700
|
+
|
701
|
+
read_file(asset_file)
|
702
|
+
else
|
703
|
+
status 404
|
704
|
+
"Asset not found: #{asset_path}"
|
705
|
+
end
|
706
|
+
rescue => e
|
707
|
+
status 500
|
708
|
+
"Error loading asset: #{e.message}"
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
# (timing route removed)
|
713
|
+
|
714
|
+
# Serve post files relative to preview route
|
715
|
+
get '/posts/:filename' do
|
716
|
+
filename = params[:filename]
|
717
|
+
@current_view = @api&.current_view
|
718
|
+
|
719
|
+
begin
|
720
|
+
if filename.nil? || filename.strip.empty? || @current_view.nil?
|
721
|
+
status 404
|
722
|
+
return "File not found"
|
723
|
+
end
|
724
|
+
|
725
|
+
# Construct the file path
|
726
|
+
post_file = @api.root/"views"/@current_view.name/"output"/"posts"/filename
|
727
|
+
|
728
|
+
if File.exist?(post_file)
|
729
|
+
content_type :html
|
730
|
+
read_file(post_file)
|
351
731
|
else
|
352
|
-
STDERR.puts "DEBUG: File not found"
|
353
732
|
status 404
|
354
733
|
"File not found: #{filename}"
|
355
734
|
end
|
356
735
|
rescue => e
|
357
|
-
STDERR.puts "DEBUG: Error: #{e.message}"
|
358
736
|
status 500
|
359
737
|
"Error loading file: #{e.message}"
|
360
738
|
end
|
361
739
|
end
|
362
740
|
|
741
|
+
# Handle direct access to posts via index.html?post=filename
|
742
|
+
get '/index.html' do
|
743
|
+
post_param = params[:post]
|
744
|
+
@current_view = @api&.current_view
|
745
|
+
|
746
|
+
begin
|
747
|
+
if @current_view.nil?
|
748
|
+
status 404
|
749
|
+
return "View not found"
|
750
|
+
end
|
751
|
+
|
752
|
+
# Always return the full index.html page
|
753
|
+
# The JavaScript will handle loading the specific post if post_param is provided
|
754
|
+
index_file = @api.root/"views"/@current_view.name/"output"/"index.html"
|
755
|
+
|
756
|
+
if File.exist?(index_file)
|
757
|
+
content_type :html
|
758
|
+
read_file(index_file)
|
759
|
+
else
|
760
|
+
status 404
|
761
|
+
"Index page not found"
|
762
|
+
end
|
763
|
+
rescue => e
|
764
|
+
status 500
|
765
|
+
"Error loading page: #{e.message}"
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
# Handle permalink access to posts
|
770
|
+
get '/permalink/:filename' do
|
771
|
+
filename = params[:filename]
|
772
|
+
@current_view = @api&.current_view
|
773
|
+
|
774
|
+
begin
|
775
|
+
if filename.nil? || filename.strip.empty? || @current_view.nil?
|
776
|
+
status 404
|
777
|
+
return "Post not found"
|
778
|
+
end
|
779
|
+
|
780
|
+
# Construct the file path
|
781
|
+
post_file = @api.root/"views"/@current_view.name/"output"/"posts"/filename
|
782
|
+
|
783
|
+
if File.exist?(post_file)
|
784
|
+
# Redirect to the index page with the post parameter
|
785
|
+
# This allows the JavaScript to handle the post loading properly
|
786
|
+
redirect "/index.html?post=#{filename}"
|
787
|
+
else
|
788
|
+
status 404
|
789
|
+
"Post not found: #{filename}"
|
790
|
+
end
|
791
|
+
rescue => e
|
792
|
+
status 500
|
793
|
+
"Error loading post: #{e.message}"
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
363
797
|
# Show view configuration page
|
364
798
|
get '/configure_view/:name' do
|
365
799
|
begin
|
@@ -411,7 +845,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
411
845
|
config_content += "theme #{params[:view_theme]}\n"
|
412
846
|
|
413
847
|
config_file = @api.root/"views"/view_name/"config.txt"
|
414
|
-
|
848
|
+
write_file(config_file, config_content)
|
415
849
|
end
|
416
850
|
|
417
851
|
# Step 2: Save layout configuration
|
@@ -438,7 +872,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
438
872
|
|
439
873
|
layout_file = @api.root/"views"/view_name/"config"/"layout.txt"
|
440
874
|
FileUtils.mkdir_p(File.dirname(layout_file))
|
441
|
-
|
875
|
+
write_file(layout_file, layout_content)
|
442
876
|
end
|
443
877
|
|
444
878
|
# Step 3: Save container content files
|
@@ -449,7 +883,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
449
883
|
if params[content_param]
|
450
884
|
content_file = @api.root/"views"/view_name/"config"/"#{container}.txt"
|
451
885
|
FileUtils.mkdir_p(File.dirname(content_file))
|
452
|
-
|
886
|
+
write_file(content_file, params[content_param])
|
453
887
|
|
454
888
|
# If this is header with "banner svg", create default svg.txt
|
455
889
|
if container == 'header' && params[content_param].strip == 'banner svg'
|
@@ -461,7 +895,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
461
895
|
default_svg_content += "back.linear #f8f9fa #e9ecef lr\n"
|
462
896
|
default_svg_content += "text.color #374151\n"
|
463
897
|
default_svg_content += "title.style bold\n"
|
464
|
-
|
898
|
+
write_file(svg_file, default_svg_content)
|
465
899
|
end
|
466
900
|
end
|
467
901
|
end
|
@@ -483,7 +917,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
483
917
|
|
484
918
|
# Get current SVG config
|
485
919
|
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
486
|
-
@svg_config = File.exist?(svg_file) ?
|
920
|
+
@svg_config = File.exist?(svg_file) ? read_file(svg_file) : ""
|
487
921
|
|
488
922
|
# Generate current banner for display
|
489
923
|
begin
|
@@ -498,7 +932,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
498
932
|
banner.parse_header_svg
|
499
933
|
end
|
500
934
|
|
501
|
-
@banner_svg = banner.
|
935
|
+
@banner_svg = banner.get_svg
|
502
936
|
rescue => e
|
503
937
|
@banner_svg = "<p>Error generating banner: #{e.message}</p>"
|
504
938
|
end
|
@@ -520,7 +954,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
520
954
|
# Save the SVG configuration
|
521
955
|
svg_file = @api.root/"views"/@current_view.name/"config"/"svg.txt"
|
522
956
|
FileUtils.mkdir_p(File.dirname(svg_file))
|
523
|
-
|
957
|
+
write_file(svg_file, svg_config)
|
524
958
|
|
525
959
|
# Update status
|
526
960
|
update_config_status(@current_view.name, "banner", true)
|
@@ -541,7 +975,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
541
975
|
|
542
976
|
# Get current navbar config
|
543
977
|
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
544
|
-
@navbar_config = File.exist?(navbar_file) ?
|
978
|
+
@navbar_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
|
545
979
|
|
546
980
|
# Generate current navbar preview
|
547
981
|
begin
|
@@ -574,7 +1008,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
574
1008
|
|
575
1009
|
# Read current navbar config
|
576
1010
|
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
577
|
-
current_config = File.exist?(navbar_file) ?
|
1011
|
+
current_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
|
578
1012
|
|
579
1013
|
# Add new item based on action
|
580
1014
|
if action == "link"
|
@@ -594,7 +1028,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
594
1028
|
|
595
1029
|
# Save the updated configuration
|
596
1030
|
FileUtils.mkdir_p(File.dirname(navbar_file))
|
597
|
-
|
1031
|
+
write_file(navbar_file, updated_config.rstrip + "\n")
|
598
1032
|
|
599
1033
|
redirect "/navbar_config?message=#{message}"
|
600
1034
|
rescue => e
|
@@ -632,7 +1066,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
632
1066
|
|
633
1067
|
# Read current navbar config
|
634
1068
|
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
635
|
-
current_config = File.exist?(navbar_file) ?
|
1069
|
+
current_config = File.exist?(navbar_file) ? read_file(navbar_file).strip : ""
|
636
1070
|
|
637
1071
|
# Find the parent and add child after it
|
638
1072
|
lines = current_config.lines
|
@@ -655,7 +1089,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
655
1089
|
|
656
1090
|
# Save the updated configuration
|
657
1091
|
FileUtils.mkdir_p(File.dirname(navbar_file))
|
658
|
-
|
1092
|
+
write_file(navbar_file, new_lines.join.rstrip + "\n")
|
659
1093
|
|
660
1094
|
redirect "/navbar_config?message=Added #{label} as child of #{parent}"
|
661
1095
|
rescue => e
|
@@ -681,7 +1115,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
681
1115
|
# Save the configuration
|
682
1116
|
navbar_file = @api.root/"views"/@current_view.name/"config"/"navbar.txt"
|
683
1117
|
FileUtils.mkdir_p(File.dirname(navbar_file))
|
684
|
-
|
1118
|
+
write_file(navbar_file, config.rstrip + "\n")
|
685
1119
|
|
686
1120
|
# Check for missing pages and create them
|
687
1121
|
pages_created = []
|
@@ -709,7 +1143,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
709
1143
|
page_file = pages_dir/filename
|
710
1144
|
unless File.exist?(page_file)
|
711
1145
|
content = ".page_title #{title}\n\n"
|
712
|
-
|
1146
|
+
write_file(page_file, content)
|
713
1147
|
pages_created << filename
|
714
1148
|
end
|
715
1149
|
end
|
@@ -730,7 +1164,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
730
1164
|
page_file = pages_dir/filename
|
731
1165
|
unless File.exist?(page_file)
|
732
1166
|
content = ".page_title #{title}\n\n"
|
733
|
-
|
1167
|
+
write_file(page_file, content)
|
734
1168
|
pages_created << filename
|
735
1169
|
end
|
736
1170
|
end
|
@@ -766,7 +1200,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
766
1200
|
Dir.glob(pages_dir/"*").each do |file|
|
767
1201
|
next unless File.file?(file)
|
768
1202
|
filename = File.basename(file)
|
769
|
-
content =
|
1203
|
+
content = read_file(file)
|
770
1204
|
|
771
1205
|
# Extract page title from .page_title directive
|
772
1206
|
title = nil
|
@@ -791,8 +1225,15 @@ class ScriptoriumWeb < Sinatra::Base
|
|
791
1225
|
|
792
1226
|
# Save page content
|
793
1227
|
post '/edit_pages/save' do
|
1228
|
+
File.write('/tmp/edit_pages_debug.log', "=== SAVE ATTEMPT ===\n", mode: 'a')
|
1229
|
+
File.write('/tmp/edit_pages_debug.log', "Time: #{Time.now}\n", mode: 'a')
|
1230
|
+
File.write('/tmp/edit_pages_debug.log', "API instance: #{@api.inspect}\n", mode: 'a')
|
1231
|
+
File.write('/tmp/edit_pages_debug.log', "Current view: #{@api&.current_view&.inspect}\n", mode: 'a')
|
1232
|
+
File.write('/tmp/edit_pages_debug.log', "Params: #{params.inspect}\n", mode: 'a')
|
1233
|
+
|
794
1234
|
@current_view = @api&.current_view
|
795
1235
|
if @current_view.nil?
|
1236
|
+
File.write('/tmp/edit_pages_debug.log', "ERROR: No current view\n", mode: 'a')
|
796
1237
|
redirect "/?error=No view selected. Please select a view first."
|
797
1238
|
return
|
798
1239
|
end
|
@@ -801,19 +1242,28 @@ class ScriptoriumWeb < Sinatra::Base
|
|
801
1242
|
filename = params[:filename]&.strip
|
802
1243
|
content = params[:content]&.strip || ""
|
803
1244
|
|
1245
|
+
File.write('/tmp/edit_pages_debug.log', "Filename: #{filename.inspect}\n", mode: 'a')
|
1246
|
+
File.write('/tmp/edit_pages_debug.log', "Content length: #{content.length}\n", mode: 'a')
|
1247
|
+
|
804
1248
|
if filename.nil? || filename.empty?
|
1249
|
+
File.write('/tmp/edit_pages_debug.log', "ERROR: Filename is empty\n", mode: 'a')
|
805
1250
|
redirect "/edit_pages?error=Filename is required"
|
806
1251
|
return
|
807
1252
|
end
|
808
1253
|
|
809
1254
|
# Save the page
|
810
1255
|
pages_dir = @api.root/"views"/@current_view.name/"pages"
|
1256
|
+
File.write('/tmp/edit_pages_debug.log', "Pages dir: #{pages_dir}\n", mode: 'a')
|
811
1257
|
FileUtils.mkdir_p(pages_dir)
|
812
1258
|
page_file = pages_dir/filename
|
1259
|
+
File.write('/tmp/edit_pages_debug.log', "Page file: #{page_file}\n", mode: 'a')
|
813
1260
|
File.write(page_file, content)
|
1261
|
+
File.write('/tmp/edit_pages_debug.log', "SUCCESS: File written\n", mode: 'a')
|
814
1262
|
|
815
1263
|
redirect "/edit_pages?message=Page '#{filename}' saved successfully"
|
816
1264
|
rescue => e
|
1265
|
+
File.write('/tmp/edit_pages_debug.log', "EXCEPTION: #{e.class}: #{e.message}\n", mode: 'a')
|
1266
|
+
File.write('/tmp/edit_pages_debug.log', "Backtrace: #{e.backtrace.first(5).join("\n")}\n", mode: 'a')
|
817
1267
|
redirect "/edit_pages?error=Failed to save page: #{e.message}"
|
818
1268
|
end
|
819
1269
|
end
|
@@ -822,68 +1272,111 @@ class ScriptoriumWeb < Sinatra::Base
|
|
822
1272
|
get '/view/:name' do
|
823
1273
|
view_name = params[:name]
|
824
1274
|
|
1275
|
+
# Debug logging
|
1276
|
+
File.write('/tmp/dashboard_debug.log', "Dashboard accessed for view: #{view_name} at #{Time.now}\n", mode: 'a')
|
1277
|
+
|
825
1278
|
begin
|
826
1279
|
# Look up the view
|
827
|
-
|
828
|
-
if
|
1280
|
+
@current_view = @api.lookup_view(view_name)
|
1281
|
+
if @current_view.nil?
|
829
1282
|
redirect "/?error=View '#{view_name}' not found"
|
830
1283
|
return
|
831
1284
|
end
|
832
1285
|
|
1286
|
+
# Get all views for the checkbox list
|
1287
|
+
@views = @api.views || []
|
1288
|
+
|
833
1289
|
# Set as current view
|
834
1290
|
@api.view(view_name)
|
835
|
-
@current_view = @api.current_view
|
1291
|
+
# @current_view = @api.current_view # This line is now redundant as @current_view is set above
|
1292
|
+
|
1293
|
+
# Auto-generate view if not already generated
|
1294
|
+
index_file = @api.root/"views"/view_name/"output"/"index.html"
|
1295
|
+
unless File.exist?(index_file)
|
1296
|
+
begin
|
1297
|
+
@api.generate_view(view_name)
|
1298
|
+
rescue => e
|
1299
|
+
# Log the error but don't fail the dashboard load
|
1300
|
+
File.write('/tmp/dashboard_debug.log', "Auto-generation failed: #{e.message}\n", mode: 'a')
|
1301
|
+
end
|
1302
|
+
end
|
836
1303
|
|
837
1304
|
# Generate banner for display
|
838
1305
|
begin
|
839
|
-
bsvg = Scriptorium::BannerSVG.new(
|
1306
|
+
bsvg = Scriptorium::BannerSVG.new(@current_view.title, @current_view.subtitle)
|
840
1307
|
svg_config_file = @api.root/"views"/view_name/"config"/"svg.txt"
|
841
1308
|
if File.exist?(svg_config_file)
|
842
|
-
|
843
|
-
config_dir = @api.root/"views"/view_name/"config"
|
844
|
-
Dir.chdir(config_dir) do
|
845
|
-
if File.exist?("config.txt")
|
846
|
-
File.rename("config.txt", "config.txt.backup")
|
847
|
-
end
|
848
|
-
File.rename("svg.txt", "config.txt")
|
849
|
-
|
850
|
-
begin
|
851
|
-
bsvg.parse_header_svg
|
852
|
-
ensure
|
853
|
-
# Restore original files
|
854
|
-
File.rename("config.txt", "svg.txt")
|
855
|
-
if File.exist?("config.txt.backup")
|
856
|
-
File.rename("config.txt.backup", "config.txt")
|
857
|
-
end
|
858
|
-
end
|
859
|
-
end
|
1309
|
+
bsvg.parse_header_svg(svg_config_file)
|
860
1310
|
else
|
861
1311
|
bsvg.parse_header_svg
|
862
1312
|
end
|
863
1313
|
# Generate responsive SVG for web display
|
864
|
-
svg_html = bsvg.
|
865
|
-
|
866
|
-
|
867
|
-
if svg_match
|
868
|
-
svg_content = svg_match[1]
|
869
|
-
# Calculate height based on aspect ratio (7.0 from config)
|
870
|
-
width = 800
|
871
|
-
height = (width / 7.0).to_i
|
872
|
-
@banner_svg = <<~HTML
|
873
|
-
<svg xmlns='http://www.w3.org/2000/svg'
|
874
|
-
width='100%' height='auto'
|
875
|
-
viewBox='0 0 #{width} #{height}'
|
876
|
-
preserveAspectRatio='xMidYMid meet'>
|
877
|
-
#{svg_content}
|
878
|
-
</svg>
|
879
|
-
HTML
|
880
|
-
else
|
881
|
-
@banner_svg = svg_html
|
882
|
-
end
|
1314
|
+
svg_html = bsvg.get_svg
|
1315
|
+
File.write('/tmp/dashboard_debug.log', "get_svg returned: #{svg_html[0..200]}...\n", mode: 'a')
|
1316
|
+
@banner_svg = svg_html
|
883
1317
|
rescue => e
|
884
1318
|
@banner_svg = "<p>Error generating banner: #{e.message}</p>"
|
885
1319
|
end
|
886
1320
|
|
1321
|
+
# Get posts for pagination
|
1322
|
+
begin
|
1323
|
+
posts = @api.posts(view_name, include_deleted: true) || []
|
1324
|
+
|
1325
|
+
# Debug: check if include_deleted is working
|
1326
|
+
File.write('/tmp/dashboard_debug.log', "Found #{posts.length} posts (including deleted)\n", mode: 'a')
|
1327
|
+
deleted_count = posts.count(&:deleted)
|
1328
|
+
File.write('/tmp/dashboard_debug.log', "Deleted posts: #{deleted_count}\n", mode: 'a')
|
1329
|
+
|
1330
|
+
# Debug: log first few posts and their dates for ordering analysis
|
1331
|
+
posts.first(5).each_with_index do |post, i|
|
1332
|
+
File.write('/tmp/dashboard_debug.log', "Post #{i}: #{post.num} - #{post.title} - date: #{post.date}\n", mode: 'a')
|
1333
|
+
end
|
1334
|
+
|
1335
|
+
posts.sort! { |a, b| post_compare(a, b) } # Sort by date, newest first
|
1336
|
+
|
1337
|
+
# Get posts per page from config, default to 10
|
1338
|
+
config_file = @api.root/"views"/view_name/"config"/"post_index.txt"
|
1339
|
+
posts_per_page = 10
|
1340
|
+
if File.exist?(config_file)
|
1341
|
+
config_content = read_file(config_file)
|
1342
|
+
if config_content.strip.length > 0
|
1343
|
+
posts_per_page = config_content.lines.first.strip.split.last.to_i
|
1344
|
+
end
|
1345
|
+
end
|
1346
|
+
|
1347
|
+
# Pagination logic
|
1348
|
+
page = (params[:page] || 1).to_i
|
1349
|
+
total_posts = posts.length
|
1350
|
+
total_pages = (total_posts.to_f / posts_per_page).ceil
|
1351
|
+
|
1352
|
+
# Debug pagination
|
1353
|
+
File.write('/tmp/dashboard_debug.log', "Page requested: #{params[:page]}, calculated: #{page}, total_pages: #{total_pages}\n", mode: 'a')
|
1354
|
+
|
1355
|
+
# Preserve current page if possible, otherwise reset to 1
|
1356
|
+
if page > total_pages && total_pages > 0
|
1357
|
+
page = total_pages
|
1358
|
+
File.write('/tmp/dashboard_debug.log', "Page adjusted to total_pages: #{page}\n", mode: 'a')
|
1359
|
+
elsif page < 1 || total_pages == 0
|
1360
|
+
page = 1
|
1361
|
+
File.write('/tmp/dashboard_debug.log', "Page reset to 1\n", mode: 'a')
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
start_index = (page - 1) * posts_per_page
|
1365
|
+
end_index = [start_index + posts_per_page - 1, total_posts - 1].min
|
1366
|
+
|
1367
|
+
@posts = posts[start_index..end_index] || []
|
1368
|
+
@current_page = page
|
1369
|
+
@total_pages = total_pages
|
1370
|
+
@total_posts = total_posts
|
1371
|
+
@posts_per_page = posts_per_page
|
1372
|
+
rescue => e
|
1373
|
+
@posts = []
|
1374
|
+
@current_page = 1
|
1375
|
+
@total_pages = 1
|
1376
|
+
@total_posts = 0
|
1377
|
+
@posts_per_page = 10
|
1378
|
+
end
|
1379
|
+
|
887
1380
|
erb :view_dashboard
|
888
1381
|
rescue => e
|
889
1382
|
redirect "/?error=Failed to load view dashboard: #{e.message}"
|
@@ -904,14 +1397,9 @@ class ScriptoriumWeb < Sinatra::Base
|
|
904
1397
|
@configs = {}
|
905
1398
|
|
906
1399
|
if File.exist?(status_file)
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
next if line.empty? || line.start_with?('#')
|
911
|
-
if line.include?(' ')
|
912
|
-
key, value = line.split(/\s+/, 2)
|
913
|
-
@configs[key.to_sym] = value == 'y'
|
914
|
-
end
|
1400
|
+
status_config = @api.parse_commented_file(status_file)
|
1401
|
+
status_config.each do |key, value|
|
1402
|
+
@configs[key.to_sym] = value == 'y'
|
915
1403
|
end
|
916
1404
|
else
|
917
1405
|
# Default to all 'n' if status file doesn't exist
|
@@ -930,16 +1418,9 @@ class ScriptoriumWeb < Sinatra::Base
|
|
930
1418
|
layout_file = config_dir/"layout.txt"
|
931
1419
|
@layout_containers = []
|
932
1420
|
if File.exist?(layout_file)
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
next if line.empty? || line.start_with?('#')
|
937
|
-
if line.include?(' ')
|
938
|
-
container = line.split(/\s+/, 2)[0]
|
939
|
-
@layout_containers << container
|
940
|
-
else
|
941
|
-
@layout_containers << line
|
942
|
-
end
|
1421
|
+
layout_config = @api.parse_commented_file(layout_file)
|
1422
|
+
layout_config.each do |container, _|
|
1423
|
+
@layout_containers << container
|
943
1424
|
end
|
944
1425
|
end
|
945
1426
|
|
@@ -958,7 +1439,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
958
1439
|
header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
|
959
1440
|
@current_config = ""
|
960
1441
|
if File.exist?(header_file)
|
961
|
-
@current_config =
|
1442
|
+
@current_config = read_file(header_file).strip
|
962
1443
|
end
|
963
1444
|
|
964
1445
|
# Parse current settings
|
@@ -988,7 +1469,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
988
1469
|
# Save the header configuration
|
989
1470
|
header_file = @api.root/"views"/@current_view.name/"config"/"header.txt"
|
990
1471
|
FileUtils.mkdir_p(File.dirname(header_file))
|
991
|
-
|
1472
|
+
write_file(header_file, header_content.join("\n") + "\n")
|
992
1473
|
|
993
1474
|
# Update status
|
994
1475
|
update_config_status(@current_view.name, "header", true)
|
@@ -1011,7 +1492,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1011
1492
|
deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
|
1012
1493
|
@deploy_config = ""
|
1013
1494
|
if File.exist?(deploy_file)
|
1014
|
-
@deploy_config =
|
1495
|
+
@deploy_config = read_file(deploy_file).strip
|
1015
1496
|
end
|
1016
1497
|
|
1017
1498
|
erb :deploy_config
|
@@ -1031,17 +1512,168 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1031
1512
|
# Save the deployment configuration
|
1032
1513
|
deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
|
1033
1514
|
FileUtils.mkdir_p(File.dirname(deploy_file))
|
1034
|
-
|
1515
|
+
write_file(deploy_file, deploy_config + "\n")
|
1035
1516
|
|
1036
1517
|
# Update status
|
1037
1518
|
update_config_status(@current_view.name, "deploy", true)
|
1038
1519
|
|
1039
|
-
|
1520
|
+
# Check if user came from deploy button - if so, auto-deploy
|
1521
|
+
if params[:from_deploy] == "1"
|
1522
|
+
# User came from deploy button, perform deployment automatically
|
1523
|
+
begin
|
1524
|
+
# Log deployment attempt
|
1525
|
+
File.open("/tmp/web_deploy.log", "a") do |f|
|
1526
|
+
f.puts "=== AUTO-DEPLOYMENT AFTER CONFIG #{Time.now} ==="
|
1527
|
+
f.puts " View name: #{@current_view.name}"
|
1528
|
+
f.puts " API object: #{@api.class}"
|
1529
|
+
f.puts " Repo root: #{@api.root}"
|
1530
|
+
end
|
1531
|
+
|
1532
|
+
# Perform deployment
|
1533
|
+
result = @api.deploy(@current_view.name)
|
1534
|
+
|
1535
|
+
# Log deployment result
|
1536
|
+
File.open("/tmp/web_deploy.log", "a") do |f|
|
1537
|
+
f.puts " Auto-deployment result: #{result}"
|
1538
|
+
f.puts " Auto-deployment completed successfully"
|
1539
|
+
end
|
1540
|
+
|
1541
|
+
if result
|
1542
|
+
redirect "/view/#{@current_view.name}?deploy_success=Deployment completed successfully&hide_uploading=1"
|
1543
|
+
else
|
1544
|
+
redirect "/view/#{@current_view.name}?error=Deployment failed&hide_uploading=1"
|
1545
|
+
end
|
1546
|
+
rescue => e
|
1547
|
+
# Log deployment error
|
1548
|
+
File.open("/tmp/web_deploy.log", "a") do |f|
|
1549
|
+
f.puts " Auto-deployment error: #{e.message}"
|
1550
|
+
f.puts " Backtrace: #{e.backtrace.first(5).join("\n ")}"
|
1551
|
+
end
|
1552
|
+
redirect "/view/#{@current_view.name}?error=Deployment configuration updated but deployment failed: #{e.message}&hide_uploading=1"
|
1553
|
+
end
|
1554
|
+
else
|
1555
|
+
# Normal case - user went to deploy config directly, just return to advanced config
|
1556
|
+
redirect "/advanced_config?message=Deployment configuration updated successfully"
|
1557
|
+
end
|
1040
1558
|
rescue => e
|
1041
1559
|
redirect "/deploy_config?error=Failed to save deployment configuration: #{e.message}"
|
1042
1560
|
end
|
1043
1561
|
end
|
1044
1562
|
|
1563
|
+
# Deploy current view
|
1564
|
+
get '/deploy' do
|
1565
|
+
@current_view = @api&.current_view
|
1566
|
+
if @current_view.nil?
|
1567
|
+
redirect "/?error=No view selected. Please select a view first."
|
1568
|
+
return
|
1569
|
+
end
|
1570
|
+
|
1571
|
+
# Check if deployment is ready
|
1572
|
+
unless @api.can_deploy?(@current_view.name)
|
1573
|
+
redirect "/deploy_config?error=View is not ready for deployment. Please configure deployment first.&from_deploy=1"
|
1574
|
+
return
|
1575
|
+
end
|
1576
|
+
|
1577
|
+
# Perform deployment directly
|
1578
|
+
begin
|
1579
|
+
# Log deployment attempt
|
1580
|
+
File.open("/tmp/web_deploy.log", "a") do |f|
|
1581
|
+
f.puts "=== WEB DEPLOYMENT ATTEMPT #{Time.now} ==="
|
1582
|
+
f.puts " View name: #{@current_view.name}"
|
1583
|
+
f.puts " API object: #{@api.class}"
|
1584
|
+
f.puts " Repo root: #{@api.root}"
|
1585
|
+
end
|
1586
|
+
|
1587
|
+
# Perform deployment
|
1588
|
+
result = @api.deploy(@current_view.name)
|
1589
|
+
|
1590
|
+
# Log deployment result
|
1591
|
+
File.open("/tmp/web_deploy.log", "a") do |f|
|
1592
|
+
f.puts " Deployment result: #{result}"
|
1593
|
+
f.puts " Deployment completed successfully"
|
1594
|
+
end
|
1595
|
+
|
1596
|
+
if result
|
1597
|
+
redirect "/view/#{@current_view.name}?deploy_success=Deployment completed successfully&hide_uploading=1"
|
1598
|
+
else
|
1599
|
+
redirect "/view/#{@current_view.name}?error=Deployment failed&hide_uploading=1"
|
1600
|
+
end
|
1601
|
+
rescue => e
|
1602
|
+
# Log deployment error
|
1603
|
+
File.open("/tmp/web_deploy.log", "a") do |f|
|
1604
|
+
f.puts " Deployment error: #{e.message}"
|
1605
|
+
f.puts " Backtrace: #{e.backtrace.first(5).join("\n ")}"
|
1606
|
+
end
|
1607
|
+
redirect "/view/#{@current_view.name}?error=Deployment failed: #{e.message}"
|
1608
|
+
end
|
1609
|
+
end
|
1610
|
+
|
1611
|
+
|
1612
|
+
|
1613
|
+
# Browse deployed view
|
1614
|
+
get '/browse' do
|
1615
|
+
@current_view = @api&.current_view
|
1616
|
+
if @current_view.nil?
|
1617
|
+
redirect "/?error=No view selected. Please select a view first."
|
1618
|
+
return
|
1619
|
+
end
|
1620
|
+
|
1621
|
+
# Check if deployment configuration exists
|
1622
|
+
deploy_file = @api.root/"views"/@current_view.name/"config"/"deploy.txt"
|
1623
|
+
unless File.exist?(deploy_file)
|
1624
|
+
redirect "/deploy_config?error=No deployment configuration found. Please configure deployment first."
|
1625
|
+
return
|
1626
|
+
end
|
1627
|
+
|
1628
|
+
# Read deployment configuration and extract domain
|
1629
|
+
deploy_config = read_file(deploy_file).strip
|
1630
|
+
if deploy_config.empty?
|
1631
|
+
redirect "/deploy_config?error=Deployment configuration is empty."
|
1632
|
+
return
|
1633
|
+
end
|
1634
|
+
|
1635
|
+
# Extract domain and path from deploy config (simple parsing)
|
1636
|
+
lines = deploy_config.split("\n")
|
1637
|
+
domain = nil
|
1638
|
+
path = nil
|
1639
|
+
lines.each do |line|
|
1640
|
+
line = line.strip
|
1641
|
+
next if line.empty? || line.start_with?('#')
|
1642
|
+
if line.match(/^(\w+)\s+(.+)$/)
|
1643
|
+
key = $1.strip
|
1644
|
+
value = $2.strip
|
1645
|
+
if key == 'proto' && value.start_with?('http')
|
1646
|
+
# Look for server and path fields to construct URL
|
1647
|
+
lines.each do |config_line|
|
1648
|
+
config_line = config_line.strip
|
1649
|
+
next if config_line.empty? || config_line.start_with?('#')
|
1650
|
+
if config_line.match(/^(\w+)\s+(.+)$/)
|
1651
|
+
config_key = $1.strip
|
1652
|
+
config_value = $2.strip
|
1653
|
+
if config_key == 'server'
|
1654
|
+
domain = "#{value}://#{config_value}"
|
1655
|
+
elsif config_key == 'path'
|
1656
|
+
path = config_value
|
1657
|
+
end
|
1658
|
+
end
|
1659
|
+
end
|
1660
|
+
break
|
1661
|
+
end
|
1662
|
+
end
|
1663
|
+
end
|
1664
|
+
|
1665
|
+
if domain
|
1666
|
+
# Append path if it exists
|
1667
|
+
if path && !path.empty?
|
1668
|
+
redirect "#{domain}/#{path}"
|
1669
|
+
else
|
1670
|
+
redirect domain
|
1671
|
+
end
|
1672
|
+
else
|
1673
|
+
redirect "/deploy_config?error=Could not extract domain from deployment configuration."
|
1674
|
+
end
|
1675
|
+
end
|
1676
|
+
|
1045
1677
|
# Layout configuration page
|
1046
1678
|
get '/layout_config' do
|
1047
1679
|
@current_view = @api&.current_view
|
@@ -1054,7 +1686,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1054
1686
|
layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
|
1055
1687
|
@layout_config = ""
|
1056
1688
|
if File.exist?(layout_file)
|
1057
|
-
@layout_config =
|
1689
|
+
@layout_config = read_file(layout_file).strip
|
1058
1690
|
end
|
1059
1691
|
|
1060
1692
|
erb :layout_config
|
@@ -1074,7 +1706,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1074
1706
|
# Save the layout configuration
|
1075
1707
|
layout_file = @api.root/"views"/@current_view.name/"config"/"layout.txt"
|
1076
1708
|
FileUtils.mkdir_p(File.dirname(layout_file))
|
1077
|
-
|
1709
|
+
write_file(layout_file, layout_config + "\n")
|
1078
1710
|
|
1079
1711
|
redirect "/advanced_config?message=Layout configuration updated successfully"
|
1080
1712
|
rescue => e
|
@@ -1082,32 +1714,34 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1082
1714
|
end
|
1083
1715
|
end
|
1084
1716
|
|
1085
|
-
# Serve
|
1086
|
-
get '/
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
"
|
1717
|
+
# Serve web app's own assets (like livetext_mode.js)
|
1718
|
+
get '/web_assets/*' do
|
1719
|
+
begin
|
1720
|
+
asset_path = params[:splat].first
|
1721
|
+
asset_file = File.join(Dir.pwd, 'ui', 'web', 'app', 'assets', asset_path)
|
1722
|
+
|
1723
|
+
puts "DEBUG: Asset path: #{asset_path}"
|
1724
|
+
puts "DEBUG: Asset file: #{asset_file}"
|
1725
|
+
puts "DEBUG: File exists: #{File.exist?(asset_file)}"
|
1726
|
+
puts "DEBUG: Is file: #{File.file?(asset_file)}"
|
1727
|
+
|
1728
|
+
if File.exist?(asset_file) && File.file?(asset_file)
|
1729
|
+
send_file asset_file
|
1730
|
+
else
|
1731
|
+
status 404
|
1732
|
+
"Web asset not found"
|
1733
|
+
end
|
1734
|
+
rescue => e
|
1735
|
+
puts "DEBUG: Exception in web_assets: #{e.class}: #{e.message}"
|
1736
|
+
puts "DEBUG: Backtrace: #{e.backtrace.first(3).join("\n")}"
|
1737
|
+
status 500
|
1738
|
+
"Internal server error: #{e.message}"
|
1095
1739
|
end
|
1096
1740
|
end
|
1097
1741
|
|
1098
|
-
#
|
1099
|
-
|
1100
|
-
|
1101
|
-
asset_path = params[:splat].first
|
1102
|
-
asset_file = @api.root/"views"/view_name/"assets"/asset_path
|
1103
|
-
|
1104
|
-
if File.exist?(asset_file) && File.file?(asset_file)
|
1105
|
-
send_file asset_file
|
1106
|
-
else
|
1107
|
-
status 404
|
1108
|
-
"Asset not found"
|
1109
|
-
end
|
1110
|
-
end
|
1742
|
+
# Static files are now served directly by Sinatra from the public_folder
|
1743
|
+
# No custom routes needed for assets
|
1744
|
+
|
1111
1745
|
|
1112
1746
|
# Server status endpoint
|
1113
1747
|
get '/status' do
|
@@ -1184,14 +1818,76 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1184
1818
|
end
|
1185
1819
|
end
|
1186
1820
|
|
1821
|
+
# Get post-specific assets
|
1822
|
+
@post_assets = []
|
1823
|
+
posts_dir = @api.root/:posts
|
1824
|
+
if Dir.exist?(posts_dir)
|
1825
|
+
Dir.glob(posts_dir/"*").each do |post_dir|
|
1826
|
+
next unless Dir.exist?(post_dir)
|
1827
|
+
post_num = File.basename(post_dir)
|
1828
|
+
next unless post_num.match?(/^\d{4}$/) # Only process 4-digit post numbers
|
1829
|
+
|
1830
|
+
post_id = post_num.to_i
|
1831
|
+
assets = @api.list_assets('post', post_id)
|
1832
|
+
assets.each do |asset|
|
1833
|
+
@post_assets << asset.merge({
|
1834
|
+
post_id: post_id,
|
1835
|
+
post_title: get_post_title(post_id)
|
1836
|
+
})
|
1837
|
+
end
|
1838
|
+
end
|
1839
|
+
end
|
1840
|
+
|
1187
1841
|
# Sort all asset lists
|
1188
1842
|
@global_assets.sort_by! { |asset| asset[:filename] }
|
1189
1843
|
@library_assets.sort_by! { |asset| asset[:filename] }
|
1190
1844
|
@view_assets.sort_by! { |asset| asset[:filename] }
|
1845
|
+
@post_assets.sort_by! { |asset| [asset[:post_id], asset[:filename]] }
|
1191
1846
|
|
1192
1847
|
erb :asset_management
|
1193
1848
|
end
|
1194
1849
|
|
1850
|
+
# Upload post-specific asset
|
1851
|
+
post '/upload_post_asset' do
|
1852
|
+
@current_view = @api&.current_view
|
1853
|
+
if @current_view.nil?
|
1854
|
+
redirect "/?error=No view selected. Please select a view first."
|
1855
|
+
return
|
1856
|
+
end
|
1857
|
+
|
1858
|
+
begin
|
1859
|
+
post_id = params[:post_id]&.to_i
|
1860
|
+
file = params[:file]
|
1861
|
+
|
1862
|
+
if post_id.nil? || post_id <= 0
|
1863
|
+
redirect "/view/#{@current_view.name}?error=Invalid post ID"
|
1864
|
+
return
|
1865
|
+
end
|
1866
|
+
|
1867
|
+
if file.nil? || file[:tempfile].nil?
|
1868
|
+
redirect "/view/#{@current_view.name}?error=No file selected"
|
1869
|
+
return
|
1870
|
+
end
|
1871
|
+
|
1872
|
+
# Check if post exists
|
1873
|
+
post = @api.post(post_id)
|
1874
|
+
if post.nil?
|
1875
|
+
redirect "/view/#{@current_view.name}?error=Post #{post_id} not found"
|
1876
|
+
return
|
1877
|
+
end
|
1878
|
+
|
1879
|
+
filename = file[:filename]
|
1880
|
+
tempfile = file[:tempfile]
|
1881
|
+
|
1882
|
+
# Use the API to upload the asset with original filename
|
1883
|
+
target_file = @api.upload_asset(tempfile.path, 'post', post_id, filename: filename)
|
1884
|
+
|
1885
|
+
redirect "/view/#{@current_view.name}?message=Asset '#{filename}' uploaded successfully to post ##{post_id}"
|
1886
|
+
rescue => e
|
1887
|
+
redirect "/view/#{@current_view.name}?error=Failed to upload asset: #{e.message}"
|
1888
|
+
end
|
1889
|
+
end
|
1890
|
+
|
1195
1891
|
# Upload asset
|
1196
1892
|
post '/asset_management/upload' do
|
1197
1893
|
@current_view = @api&.current_view
|
@@ -1327,12 +2023,151 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1327
2023
|
end
|
1328
2024
|
end
|
1329
2025
|
|
2026
|
+
# Widget Management Routes
|
2027
|
+
|
2028
|
+
# List widgets for current view
|
2029
|
+
get '/widgets' do
|
2030
|
+
if @api&.instance_variable_get(:@repo) && @current_view
|
2031
|
+
@available_widgets = @api.widgets_available
|
2032
|
+
@configured_widgets = []
|
2033
|
+
@widget_containers = {}
|
2034
|
+
|
2035
|
+
@available_widgets.each do |widget|
|
2036
|
+
widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget
|
2037
|
+
if Dir.exist?(widget_dir)
|
2038
|
+
@configured_widgets << widget
|
2039
|
+
# Determine which container this widget is in
|
2040
|
+
@widget_containers[widget] = determine_widget_container(@current_view)
|
2041
|
+
end
|
2042
|
+
end
|
2043
|
+
|
2044
|
+
erb :widgets
|
2045
|
+
else
|
2046
|
+
redirect "/?error=No repository or view selected"
|
2047
|
+
end
|
2048
|
+
end
|
2049
|
+
|
2050
|
+
# Add widget to current view
|
2051
|
+
post '/add_widget' do
|
2052
|
+
if @api&.instance_variable_get(:@repo) && @current_view
|
2053
|
+
widget_name = params[:widget_name]&.strip
|
2054
|
+
|
2055
|
+
if widget_name.nil? || widget_name.empty?
|
2056
|
+
redirect "/widgets?error=Widget name required"
|
2057
|
+
return
|
2058
|
+
end
|
2059
|
+
|
2060
|
+
# Check if widget is available
|
2061
|
+
available_widgets = @api.widgets_available
|
2062
|
+
unless available_widgets.include?(widget_name)
|
2063
|
+
redirect "/widgets?error=Widget '#{widget_name}' not available"
|
2064
|
+
return
|
2065
|
+
end
|
2066
|
+
|
2067
|
+
# Check if widget is already configured
|
2068
|
+
widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget_name
|
2069
|
+
if Dir.exist?(widget_dir)
|
2070
|
+
redirect "/widgets?error=Widget '#{widget_name}' already configured"
|
2071
|
+
return
|
2072
|
+
end
|
2073
|
+
|
2074
|
+
# Determine container (left/right) for widget placement
|
2075
|
+
container = determine_widget_container(@current_view)
|
2076
|
+
unless container
|
2077
|
+
redirect "/widgets?error=No left or right container found in layout. Add a left or right container to your layout first."
|
2078
|
+
return
|
2079
|
+
end
|
2080
|
+
|
2081
|
+
# Create widget directory and list.txt
|
2082
|
+
FileUtils.mkdir_p(widget_dir)
|
2083
|
+
list_file = widget_dir/"list.txt"
|
2084
|
+
File.write(list_file, "# Add #{widget_name} items here\n")
|
2085
|
+
|
2086
|
+
# Generate the widget after creation
|
2087
|
+
begin
|
2088
|
+
@api.generate_widget(widget_name)
|
2089
|
+
redirect "/widgets?message=Widget '#{widget_name}' added successfully to #{container} container and generated"
|
2090
|
+
rescue => e
|
2091
|
+
# Widget created but generation failed
|
2092
|
+
redirect "/widgets?message=Widget '#{widget_name}' added successfully to #{container} container, but generation failed: #{e.message}"
|
2093
|
+
end
|
2094
|
+
else
|
2095
|
+
redirect "/?error=No repository or view selected"
|
2096
|
+
end
|
2097
|
+
end
|
2098
|
+
|
2099
|
+
# Configure widget data
|
2100
|
+
get '/config_widget/:widget_name' do
|
2101
|
+
if @api&.instance_variable_get(:@repo) && @current_view
|
2102
|
+
@widget_name = params[:widget_name]
|
2103
|
+
widget_dir = @api.root/"views"/@current_view.name/"widgets"/@widget_name
|
2104
|
+
|
2105
|
+
unless Dir.exist?(widget_dir)
|
2106
|
+
redirect "/widgets?error=Widget '#{@widget_name}' not configured"
|
2107
|
+
return
|
2108
|
+
end
|
2109
|
+
|
2110
|
+
list_file = widget_dir/"list.txt"
|
2111
|
+
@widget_data = File.exist?(list_file) ? File.read(list_file) : ""
|
2112
|
+
|
2113
|
+
erb :config_widget
|
2114
|
+
else
|
2115
|
+
redirect "/?error=No repository or view selected"
|
2116
|
+
end
|
2117
|
+
end
|
2118
|
+
|
2119
|
+
# Update widget data
|
2120
|
+
post '/update_widget/:widget_name' do
|
2121
|
+
if @api&.instance_variable_get(:@repo) && @current_view
|
2122
|
+
widget_name = params[:widget_name]
|
2123
|
+
widget_data = params[:widget_data]
|
2124
|
+
|
2125
|
+
widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget_name
|
2126
|
+
list_file = widget_dir/"list.txt"
|
2127
|
+
|
2128
|
+
File.write(list_file, widget_data)
|
2129
|
+
|
2130
|
+
# Generate the widget after updating
|
2131
|
+
begin
|
2132
|
+
@api.generate_widget(widget_name)
|
2133
|
+
redirect "/widgets?message=Widget '#{widget_name}' updated and generated successfully"
|
2134
|
+
rescue => e
|
2135
|
+
# Widget updated but generation failed
|
2136
|
+
redirect "/widgets?error=Widget '#{widget_name}' updated successfully, but generation failed: #{e.message}"
|
2137
|
+
end
|
2138
|
+
else
|
2139
|
+
redirect "/?error=No repository or view selected"
|
2140
|
+
end
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
# Remove widget from current view
|
2144
|
+
post '/remove_widget' do
|
2145
|
+
if @api&.instance_variable_get(:@repo) && @current_view
|
2146
|
+
widget_name = params[:widget_name]&.strip
|
2147
|
+
|
2148
|
+
if widget_name.nil? || widget_name.empty?
|
2149
|
+
redirect "/widgets?error=Widget name required"
|
2150
|
+
return
|
2151
|
+
end
|
2152
|
+
|
2153
|
+
widget_dir = @api.root/"views"/@current_view.name/"widgets"/widget_name
|
2154
|
+
if Dir.exist?(widget_dir)
|
2155
|
+
FileUtils.rm_rf(widget_dir)
|
2156
|
+
redirect "/widgets?message=Widget '#{widget_name}' removed successfully"
|
2157
|
+
else
|
2158
|
+
redirect "/widgets?error=Widget '#{widget_name}' not found"
|
2159
|
+
end
|
2160
|
+
else
|
2161
|
+
redirect "/?error=No repository or view selected"
|
2162
|
+
end
|
2163
|
+
end
|
2164
|
+
|
1330
2165
|
# Helper method to update status
|
1331
2166
|
private def update_config_status(view_name, config_name, status)
|
1332
2167
|
status_file = @api.root/"views"/view_name/"config"/"status.txt"
|
1333
2168
|
return unless File.exist?(status_file)
|
1334
2169
|
|
1335
|
-
|
2170
|
+
content = read_file(status_file)
|
1336
2171
|
lines = content.lines.map do |line|
|
1337
2172
|
if line.strip.start_with?("#{config_name} ")
|
1338
2173
|
"#{config_name} #{status ? 'y' : 'n'}\n"
|
@@ -1340,7 +2175,7 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1340
2175
|
line
|
1341
2176
|
end
|
1342
2177
|
end
|
1343
|
-
|
2178
|
+
write_file(status_file, lines.join)
|
1344
2179
|
end
|
1345
2180
|
|
1346
2181
|
# Helper method for formatting file sizes
|
@@ -1352,8 +2187,20 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1352
2187
|
"#{(bytes / k**i.to_f).round(2)} #{sizes[i]}"
|
1353
2188
|
end
|
1354
2189
|
|
2190
|
+
# Helper method to determine which container (left/right) widgets should be placed in
|
2191
|
+
private def determine_widget_container(view)
|
2192
|
+
layout_file = @api.root/"views"/view.name/"config"/"layout.txt"
|
2193
|
+
return nil unless File.exist?(layout_file)
|
2194
|
+
|
2195
|
+
layout_config = @api.parse_commented_file(layout_file)
|
2196
|
+
containers = layout_config.keys
|
2197
|
+
|
2198
|
+
# Prefer left container, fall back to right
|
2199
|
+
containers.find { |c| c == 'left' } || containers.find { |c| c == 'right' }
|
2200
|
+
end
|
2201
|
+
|
1355
2202
|
def get_image_dimensions(file_path)
|
1356
|
-
return nil unless File.exist?(file_path)
|
2203
|
+
return nil unless file_path && File.exist?(file_path)
|
1357
2204
|
|
1358
2205
|
# Check if it's an image file
|
1359
2206
|
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp']
|
@@ -1362,17 +2209,392 @@ class ScriptoriumWeb < Sinatra::Base
|
|
1362
2209
|
# Check if FastImage is available
|
1363
2210
|
return nil unless defined?(FastImage)
|
1364
2211
|
|
2212
|
+
dimensions = FastImage.size(file_path)
|
2213
|
+
return dimensions ? "#{dimensions[0]}×#{dimensions[1]}" : nil
|
2214
|
+
rescue => e
|
2215
|
+
# If FastImage fails, return nil
|
2216
|
+
return nil
|
2217
|
+
end
|
2218
|
+
|
2219
|
+
def get_post_title(post_id)
|
2220
|
+
begin
|
2221
|
+
post = @api.post(post_id)
|
2222
|
+
post.title
|
2223
|
+
rescue => e
|
2224
|
+
"Post ##{post_id}"
|
2225
|
+
end
|
2226
|
+
end
|
2227
|
+
|
2228
|
+
def format_backup_age(timestamp)
|
2229
|
+
# Parse timestamp (format: YYYYMMDD-HHMMSS)
|
2230
|
+
year = timestamp[0..3].to_i
|
2231
|
+
month = timestamp[4..5].to_i
|
2232
|
+
day = timestamp[6..7].to_i
|
2233
|
+
hour = timestamp[9..10].to_i
|
2234
|
+
minute = timestamp[11..12].to_i
|
2235
|
+
second = timestamp[13..14].to_i
|
2236
|
+
|
2237
|
+
backup_time = Time.new(year, month, day, hour, minute, second)
|
2238
|
+
now = Time.now
|
2239
|
+
diff = now - backup_time
|
2240
|
+
|
2241
|
+
if diff < 60
|
2242
|
+
"#{diff.to_i} seconds ago"
|
2243
|
+
elsif diff < 3600
|
2244
|
+
"#{(diff / 60).to_i} minutes ago"
|
2245
|
+
elsif diff < 86400
|
2246
|
+
"#{(diff / 3600).to_i} hours ago"
|
2247
|
+
elsif diff < 2592000 # 30 days
|
2248
|
+
"#{(diff / 86400).to_i} days ago"
|
2249
|
+
else
|
2250
|
+
"#{(diff / 2592000).to_i} months ago"
|
2251
|
+
end
|
2252
|
+
end
|
2253
|
+
|
2254
|
+
|
2255
|
+
|
2256
|
+
# Delete a post (move to _postnum directory)
|
2257
|
+
post '/delete_post/:id' do
|
2258
|
+
post_id = params[:id]
|
2259
|
+
|
2260
|
+
begin
|
2261
|
+
# Set current view before proceeding
|
2262
|
+
@current_view = @api&.current_view
|
2263
|
+
if @current_view.nil?
|
2264
|
+
redirect "/?error=No view selected"
|
2265
|
+
return
|
2266
|
+
end
|
2267
|
+
|
2268
|
+
post = @api.post(post_id.to_i)
|
2269
|
+
if post.nil?
|
2270
|
+
redirect "/?error=Post #{post_id} not found"
|
2271
|
+
return
|
2272
|
+
end
|
2273
|
+
|
2274
|
+
# Mark as deleted in metadata
|
2275
|
+
post.deleted = true
|
2276
|
+
|
2277
|
+
# Move post directory to _postnum
|
2278
|
+
post_dir = @api.root/"posts"/post.num
|
2279
|
+
deleted_dir = @api.root/"posts"/"_#{post.num}"
|
2280
|
+
|
2281
|
+
if Dir.exist?(post_dir)
|
2282
|
+
FileUtils.mkdir_p(File.dirname(deleted_dir))
|
2283
|
+
FileUtils.mv(post_dir, deleted_dir)
|
2284
|
+
else
|
2285
|
+
redirect "/?error=Post directory #{post_dir} not found"
|
2286
|
+
return
|
2287
|
+
end
|
2288
|
+
|
2289
|
+
# Preserve current page if available
|
2290
|
+
current_page = params[:page] || request.env['HTTP_REFERER']&.match(/[?&]page=(\d+)/)&.[](1) || 1
|
2291
|
+
redirect "/view/#{@current_view.name}?page=#{current_page}&message=Post #{post_id} deleted successfully"
|
2292
|
+
rescue => e
|
2293
|
+
redirect "/?error=Failed to delete post: #{e.message}"
|
2294
|
+
end
|
2295
|
+
end
|
2296
|
+
|
2297
|
+
# Restore a deleted post
|
2298
|
+
post '/restore_post/:id' do
|
2299
|
+
post_id = params[:id]
|
2300
|
+
|
2301
|
+
begin
|
2302
|
+
# Set current view before proceeding
|
2303
|
+
@current_view = @api&.current_view
|
2304
|
+
if @current_view.nil?
|
2305
|
+
redirect "/?error=No view selected"
|
2306
|
+
return
|
2307
|
+
end
|
2308
|
+
|
2309
|
+
# Find the deleted post directory
|
2310
|
+
formatted_id = post_id.to_s.rjust(4, '0') # Ensure 4-digit format (e.g., "28" -> "0028")
|
2311
|
+
deleted_dir = @api.root/"posts"/"_#{formatted_id}"
|
2312
|
+
post_dir = @api.root/"posts"/formatted_id
|
2313
|
+
|
2314
|
+
if Dir.exist?(deleted_dir)
|
2315
|
+
# Move back to normal posts directory
|
2316
|
+
FileUtils.mv(deleted_dir, post_dir)
|
2317
|
+
|
2318
|
+
# Update metadata to mark as not deleted
|
2319
|
+
post = @api.post(post_id.to_i)
|
2320
|
+
if post
|
2321
|
+
# Debug: log both date fields before and after
|
2322
|
+
File.write('/tmp/restore_debug.log', "Restoring post #{post_id}: pubdate before = #{post.pubdate}, created before = #{post.created}\n", mode: 'a')
|
2323
|
+
post.deleted = false
|
2324
|
+
File.write('/tmp/restore_debug.log', "Restoring post #{post_id}: pubdate after = #{post.pubdate}, created after = #{post.created}\n", mode: 'a')
|
2325
|
+
end
|
2326
|
+
|
2327
|
+
# Preserve current page if available
|
2328
|
+
current_page = params[:page] || request.env['HTTP_REFERER']&.match(/[?&]page=(\d+)/)&.[](1) || 1
|
2329
|
+
redirect "/view/#{@current_view.name}?page=#{current_page}&message=Post #{post_id} restored successfully"
|
2330
|
+
else
|
2331
|
+
redirect "/?error=Deleted post #{post_id} not found"
|
2332
|
+
end
|
2333
|
+
rescue => e
|
2334
|
+
redirect "/?error=Failed to restore post: #{e.message}"
|
2335
|
+
end
|
2336
|
+
end
|
2337
|
+
|
2338
|
+
# Toggle post published status
|
2339
|
+
post '/toggle_post_status/:id' do
|
2340
|
+
post_id = params[:id]
|
2341
|
+
|
2342
|
+
begin
|
2343
|
+
# Set current view before proceeding
|
2344
|
+
@current_view = @api&.current_view
|
2345
|
+
if @current_view.nil?
|
2346
|
+
redirect "/?error=No view selected"
|
2347
|
+
return
|
2348
|
+
end
|
2349
|
+
|
2350
|
+
post = @api.post(post_id.to_i)
|
2351
|
+
if post.nil?
|
2352
|
+
redirect "/?error=Post #{post_id} not found"
|
2353
|
+
return
|
2354
|
+
end
|
2355
|
+
|
2356
|
+
# Toggle between published and unpublished
|
2357
|
+
if post.meta["post.published"] == "no" || post.meta["post.published"].nil?
|
2358
|
+
# Publish the post - only change published status, don't touch pubdate
|
2359
|
+
post.meta["post.published"] = "yes"
|
2360
|
+
post.save_metadata
|
2361
|
+
content_type :json
|
2362
|
+
{ success: true, message: "Post #{post_id} published successfully", published: true }.to_json
|
2363
|
+
else
|
2364
|
+
# Unpublish the post - only change published status, don't touch pubdate
|
2365
|
+
post.meta["post.published"] = "no"
|
2366
|
+
post.save_metadata
|
2367
|
+
content_type :json
|
2368
|
+
{ success: true, message: "Post #{post_id} unpublished successfully", published: false }.to_json
|
2369
|
+
end
|
2370
|
+
rescue => e
|
2371
|
+
redirect "/?error=Failed to toggle post status: #{e.message}"
|
2372
|
+
end
|
2373
|
+
end
|
2374
|
+
|
2375
|
+
# Backup management
|
2376
|
+
get '/backup_management' do
|
2377
|
+
@current_view = @api&.current_view
|
2378
|
+
if @current_view.nil?
|
2379
|
+
redirect "/?error=No view selected. Please select a view first."
|
2380
|
+
return
|
2381
|
+
end
|
2382
|
+
|
2383
|
+
begin
|
2384
|
+
@backups = @api.list_backups
|
2385
|
+
# Sort backups by timestamp (newest first)
|
2386
|
+
@backups.sort_by! { |backup| backup[:timestamp] }.reverse!
|
2387
|
+
|
2388
|
+
# Add human-readable age to each backup
|
2389
|
+
@backups.each do |backup|
|
2390
|
+
# Convert Time object to string if needed
|
2391
|
+
timestamp_str = backup[:timestamp].is_a?(Time) ? backup[:timestamp].strftime("%Y%m%d-%H%M%S") : backup[:timestamp]
|
2392
|
+
backup[:age] = format_backup_age(timestamp_str)
|
2393
|
+
# Also ensure timestamp is a string for display
|
2394
|
+
backup[:timestamp] = timestamp_str
|
2395
|
+
end
|
2396
|
+
rescue => e
|
2397
|
+
@backups = []
|
2398
|
+
@error = "Failed to load backups: #{e.message}"
|
2399
|
+
end
|
2400
|
+
|
2401
|
+
erb :backup_management
|
2402
|
+
end
|
2403
|
+
|
2404
|
+
# Create backup
|
2405
|
+
post '/backup_management/create' do
|
2406
|
+
@current_view = @api&.current_view
|
2407
|
+
if @current_view.nil?
|
2408
|
+
redirect "/backup_management?error=No view selected. Please select a view first."
|
2409
|
+
return
|
2410
|
+
end
|
2411
|
+
|
2412
|
+
begin
|
2413
|
+
type = params[:type] # 'full' or 'incr'
|
2414
|
+
description = params[:description]
|
2415
|
+
|
2416
|
+
# Validate type
|
2417
|
+
unless %w[full incr].include?(type)
|
2418
|
+
redirect "/backup_management?error=Invalid backup type. Must be 'full' or 'incr'."
|
2419
|
+
return
|
2420
|
+
end
|
2421
|
+
|
2422
|
+
# Create backup
|
2423
|
+
timestamp = @api.create_backup(type: type.to_sym, label: description)
|
2424
|
+
|
2425
|
+
redirect "/backup_management?message=Backup created successfully: #{timestamp}"
|
2426
|
+
rescue => e
|
2427
|
+
redirect "/backup_management?error=Failed to create backup: #{e.message}"
|
2428
|
+
end
|
2429
|
+
end
|
2430
|
+
|
2431
|
+
# Theme management routes
|
2432
|
+
|
2433
|
+
# Theme management page
|
2434
|
+
get '/theme_management' do
|
2435
|
+
begin
|
2436
|
+
@themes = @api.themes_available
|
2437
|
+
@system_themes = @api.system_themes
|
2438
|
+
@user_themes = @api.user_themes
|
2439
|
+
erb :theme_management
|
2440
|
+
rescue => e
|
2441
|
+
redirect "/?error=Failed to load themes: #{e.message}"
|
2442
|
+
end
|
2443
|
+
end
|
2444
|
+
|
2445
|
+
# Clone theme
|
2446
|
+
post '/theme_management/clone' do
|
1365
2447
|
begin
|
1366
|
-
|
1367
|
-
|
2448
|
+
source_theme = params[:source_theme]&.strip
|
2449
|
+
new_name = params[:new_name]&.strip
|
2450
|
+
|
2451
|
+
if source_theme.nil? || source_theme.empty?
|
2452
|
+
redirect "/theme_management?error=Source theme is required"
|
2453
|
+
return
|
2454
|
+
end
|
2455
|
+
|
2456
|
+
if new_name.nil? || new_name.empty?
|
2457
|
+
redirect "/theme_management?error=New theme name is required"
|
2458
|
+
return
|
2459
|
+
end
|
2460
|
+
|
2461
|
+
# Validate new name format
|
2462
|
+
unless new_name.match?(/^[a-zA-Z0-9_-]+$/)
|
2463
|
+
redirect "/theme_management?error=Theme name can only contain letters, numbers, hyphens, and underscores"
|
2464
|
+
return
|
2465
|
+
end
|
2466
|
+
|
2467
|
+
# Clone the theme
|
2468
|
+
@api.clone_theme(source_theme, new_name)
|
2469
|
+
redirect "/theme_management?message=Theme '#{new_name}' cloned successfully from '#{source_theme}'"
|
1368
2470
|
rescue => e
|
1369
|
-
|
1370
|
-
|
2471
|
+
error_info = friendly_error_message(e)
|
2472
|
+
redirect "/theme_management?error=#{error_info[:message]}&suggestion=#{error_info[:suggestion]}"
|
1371
2473
|
end
|
1372
2474
|
end
|
2475
|
+
|
2476
|
+
# Delete theme (user themes only)
|
2477
|
+
post '/theme_management/delete' do
|
2478
|
+
begin
|
2479
|
+
theme_name = params[:theme_name]&.strip
|
2480
|
+
|
2481
|
+
if theme_name.nil? || theme_name.empty?
|
2482
|
+
redirect "/theme_management?error=Theme name is required"
|
2483
|
+
return
|
2484
|
+
end
|
2485
|
+
|
2486
|
+
# Check if it's a system theme
|
2487
|
+
if @api.system_themes.include?(theme_name)
|
2488
|
+
redirect "/theme_management?error=Cannot delete system theme '#{theme_name}'"
|
2489
|
+
return
|
2490
|
+
end
|
2491
|
+
|
2492
|
+
# Check if it's a user theme
|
2493
|
+
unless @api.user_themes.include?(theme_name)
|
2494
|
+
redirect "/theme_management?error=Theme '#{theme_name}' not found or not a user theme"
|
2495
|
+
return
|
2496
|
+
end
|
2497
|
+
|
2498
|
+
# Delete the theme directory
|
2499
|
+
theme_dir = @api.root/:themes/theme_name
|
2500
|
+
if Dir.exist?(theme_dir)
|
2501
|
+
FileUtils.rm_rf(theme_dir)
|
2502
|
+
redirect "/theme_management?message=Theme '#{theme_name}' deleted successfully"
|
2503
|
+
else
|
2504
|
+
redirect "/theme_management?error=Theme directory not found"
|
2505
|
+
end
|
2506
|
+
rescue => e
|
2507
|
+
redirect "/theme_management?error=Failed to delete theme: #{e.message}"
|
2508
|
+
end
|
2509
|
+
end
|
2510
|
+
|
2511
|
+
# Edit theme files
|
2512
|
+
get '/edit_theme/:theme_name' do
|
2513
|
+
begin
|
2514
|
+
theme_name = params[:theme_name]&.strip
|
2515
|
+
unless @api.theme_exists?(theme_name)
|
2516
|
+
redirect "/theme_management?error=Theme '#{theme_name}' not found."
|
2517
|
+
end
|
2518
|
+
|
2519
|
+
@theme_name = theme_name
|
2520
|
+
@theme_dir = @api.root/:themes/theme_name
|
2521
|
+
|
2522
|
+
# List all editable files within the theme directory
|
2523
|
+
@editable_files = []
|
2524
|
+
if Dir.exist?(@theme_dir)
|
2525
|
+
Dir.glob(File.join(@theme_dir, "**", "*.txt")).each do |file_path|
|
2526
|
+
relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(@theme_dir)).to_s
|
2527
|
+
@editable_files << relative_path
|
2528
|
+
end
|
2529
|
+
end
|
2530
|
+
|
2531
|
+
erb :edit_theme
|
2532
|
+
rescue => e
|
2533
|
+
"Error in edit_theme route: #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}"
|
2534
|
+
end
|
2535
|
+
end
|
2536
|
+
|
2537
|
+
# Edit specific theme file
|
2538
|
+
get '/edit_theme/:theme_name/*' do
|
2539
|
+
theme_name = params[:theme_name]&.strip
|
2540
|
+
file_path_param = params[:splat].first
|
2541
|
+
|
2542
|
+
unless @api.theme_exists?(theme_name)
|
2543
|
+
redirect "/theme_management?error=Theme '#{theme_name}' not found."
|
2544
|
+
end
|
2545
|
+
|
2546
|
+
theme_file_path = File.join(@api.root/:themes/theme_name, file_path_param)
|
2547
|
+
|
2548
|
+
unless File.exist?(theme_file_path)
|
2549
|
+
redirect "/edit_theme/#{theme_name}?error=File '#{file_path_param}' not found in theme '#{theme_name}'."
|
2550
|
+
end
|
2551
|
+
|
2552
|
+
@theme_name = theme_name
|
2553
|
+
@file_name = file_path_param
|
2554
|
+
@file_content = File.read(theme_file_path)
|
2555
|
+
|
2556
|
+
erb :edit_theme_file
|
2557
|
+
end
|
2558
|
+
|
2559
|
+
# Save theme file
|
2560
|
+
post '/save_theme_file/:theme_name/*' do
|
2561
|
+
theme_name = params[:theme_name]&.strip
|
2562
|
+
file_path_param = params[:splat].first
|
2563
|
+
file_content = params[:content]
|
2564
|
+
|
2565
|
+
unless @api.theme_exists?(theme_name)
|
2566
|
+
redirect "/theme_management?error=Theme '#{theme_name}' not found."
|
2567
|
+
end
|
2568
|
+
|
2569
|
+
# Ensure it's a user theme if we're allowing edits
|
2570
|
+
unless @api.user_themes.include?(theme_name)
|
2571
|
+
redirect "/edit_theme/#{theme_name}?error=Cannot edit system theme files directly."
|
2572
|
+
end
|
2573
|
+
|
2574
|
+
theme_file_path = File.join(@api.root/:themes/theme_name, file_path_param)
|
2575
|
+
|
2576
|
+
unless File.exist?(theme_file_path)
|
2577
|
+
redirect "/edit_theme/#{theme_name}?error=File '#{file_path_param}' not found in theme '#{theme_name}'."
|
2578
|
+
end
|
2579
|
+
|
2580
|
+
File.write(theme_file_path, file_content)
|
2581
|
+
redirect "/edit_theme/#{theme_name}/#{file_path_param}?message=File saved successfully."
|
2582
|
+
rescue => e
|
2583
|
+
redirect "/edit_theme/#{theme_name}/#{file_path_param}?error=Failed to save file: #{e.message}"
|
2584
|
+
end
|
2585
|
+
|
2586
|
+
# Debug route to verify code is updated
|
2587
|
+
get '/debug' do
|
2588
|
+
"Server is running updated code at #{Time.now}"
|
2589
|
+
end
|
2590
|
+
|
2591
|
+
|
1373
2592
|
end
|
1374
2593
|
|
1375
2594
|
# Start the server if this file is run directly
|
1376
2595
|
if __FILE__ == $0
|
1377
2596
|
ScriptoriumWeb.run!
|
1378
|
-
end
|
2597
|
+
end
|
2598
|
+
|
2599
|
+
# Set initial test mode from command line after class definition
|
2600
|
+
ScriptoriumWeb.test_mode = TEST_MODE
|